simulating/replicating Blackboard "template variables" in Canvas

VVMarshburn
Community Explorer
3
2120

Our institution is just completing a migration from Blackboard to Canvas. While Canvas appears to offer some very useful capabilities for our purposes, there may be some features of Blackboard that we miss somewhat. Among these are the "template variables" that could be utilized to embed user or course information into the content – e.g., by adding a code such as "@X@user.full_name@X@@" to content items including announcements, assignments, or module pages, the user's name would appear when rendered by the Web browser. This type of functionality can be helpful in customizing and personalizing content in an LMS, or automating repeated information such as a course name. 

To demonstrate, one would be able to construct the following content in Blackboard:

[image] template variables in Blackboard (code)

and have it rendered thus:

[image] template variables in Blackboard (rendered)

By default, Canvas does not currently include this kind of functionality, but thanks to some insight from resourceful members of the Instructure forums (namely user "dtod," whose code I borrowed) and the rest of the Internet (and maybe a very little help from ChatGPT), I have been able to replicate something similar in our particular instance of the LMS. This requires access to the Themes feature of your Canvas instance, where you will need to add/modify custom Javascript. (This does require admin rights, so if you do not possess this ability, you may need to try begging/pleading/cajoling/bribing your own powers-that-be as necessary.)

The premise of this code is based on the existence of what Canvas refers to as "environment variables" or ENV variables, which in this context could be made to provide similar functionality as Blackboard's template variables. (These are apparently commonly used with LTI, but we can also exploit them otherwise.)

If one were to investigate ENV variables in a Web browser's console while viewing a course page in Canvas, one would find many potentially interesting and useful bits of data that could be leveraged. However, it seems there is not necessarily an overall consistency in how some of these variables are implemented between different course pages or contexts.

For instance, "ENV.current_user" is a default system ENV variable that seems to be available in just about every context, and its properties, such as ".id" (user's Canvas ID) or ".display_name" (user's full name), can be regularly accessed. One may also observe that there appears to be a number of standalone variables derived from or related to "ENV.current_user," such as "ENV.current_user_id," which could also prove to be useful.

On the other hand, variables related to course information may not be as consistently available or accessible across content. For example, on a course home page, the "ENV.COURSE" variable would provide some very practical properties such as ".id" (course Canvas ID) and ".long_name" (full name of course), and there are some other seemingly related "shortcut" variables such as "ENV.COURSE_ID" and "ENV.COURSE_TITLE." However, as it turns out, all of these are not necessarily generated for every context.

The purpose of this code is to identify specific ENV variables that could be useful as template placeholders and to automatically substitute their actual values while a page is rendered in a Web browser. The inherent flexibility of this technique is that ultimately we can identify or define as many variables as we think we need.

The actual Javascript presented here is the result of extrapolation from various sources. Currently, it provides a reasonably working functionality that could no doubt stand any amount of improvement. Javascript is honestly not my current forte (I have been immersed in python for the past six years or so).

So, if you are willing to conduct some experimentation of your own, you would need to add the following to a Javascript file that would be uploaded to one of the Themes in your Canvas instance, which would then be selected as the current theme. (See link above regarding custom Javascript in Canvas.)

First we need to specify the variables we want to utilize (some may be system default ENV variables, some will be defined by us).

 

var envVars = ['ENV.current_user.display_name', 'ENV.courseID', 'ENV.current_user_login', 'ENV.course_name'];

 

In this case, we will ultimately utilize "ENV.current_user.display_name" as is, since it appears to be accessible throughout Canvas content; whereas for the others, while ENV variables exist that would include such data, they are not consistently defined by Canvas itself in every context, so we are defining them independently; this will help ensure we will have them available throughout our sessions. We will refer to these as "non-default" ENV variables.

We will need a function for waiting until certain conditions are met (e.g., values assigned to variables); this is sometimes referred to as a "promise" function in Javascript terminology.

 

async function waitUntil(condition, time = 100) {
    while (!condition()) {
        await new Promise((resolve) => setTimeout(resolve, time));
    }
}

 

Next, we set up the function to define and derive the non-default ENV variables (theoretically, we can create as many of these non-default items as needed; we would need to include them in the var envVars statement above as well). This will be the function referenced in the event listener that will initiate this entire process.

 

async function getVariables() {
    //non-default/derived ENV variables
    ENV.courseID = window.location.pathname.split('/')[2]; //this can be easily extracted from the current URL/path when viewing a course; this iteration of this data should persist across content
    ENV.course_name = ''; //not quite the same as ENV.COURSE.long_name, which is only available in limited contexts
    ENV.current_user_login = ''; //we sometimes find it useful to have access to the user's login name

    //using API, get current user's profile and assign property 'login_id' value to ENV.current_user_login
    response = await fetch('/api/v1/users/self/profile');
    profile = await response.json();
    ENV.current_user_login = profile.login_id.toLowerCase();
    await waitUntil(() => ENV.current_user_login != ''); //call waitUntil to ensure variable has a value

    //using API, get course info and assign property 'name' value to ENV.course_name
    response = await fetch('/api/v1/courses/' + ENV.courseID);
    course = await response.json();
    ENV.course_name = course.name;
    await waitUntil(() => ENV.course_name != '');

    //call the function(s) to replace each instance of an ENV variable in a page's content with its value
    replaceInPage();
}

 

The next function is the one called at the end of getVariables(); it identifies page element(s) to be included in the substitution operation (mainly those using the class "ic-Layout-contentWrapper"; conceivably, you could include other page elements as well). Many thanks to Instructure Community Contributor "dtod" for this code.

 

function replaceInPage(){
    var user_content = document.getElementsByClassName('ic-Layout-contentWrapper');
    Array.prototype.forEach.call(user_content, function(el) {
        for (var i=0; i<envVars.length; i++){
            var searchFor = envVars[i];
            var re = new RegExp(searchFor, "g");
            
            //call function to perform actual replacement
            replaceInText(el, re, eval(envVars[i]));
        }
    });
}

 

The function called at the end of replaceInPage() performs the actual search and replace of ENV variable strings in course page content; again, many thanks to Instructure Community Contributor "dtod" for this code.

 

function replaceInText(element, pattern, replacement) {
    for (let node of element.childNodes) {
        switch (node.nodeType) {
            case Node.ELEMENT_NODE:
            attr='href';
            if (node.hasAttribute(attr)){
                node.setAttribute(attr, node.getAttribute(attr).replace(pattern, replacement));
            }
            replaceInText(node, pattern, replacement);
            break;
            case Node.TEXT_NODE:
                node.textContent = node.textContent.replace(pattern, replacement);
            break;
            case Node.DOCUMENT_NODE:
                replaceInText(node, pattern, replacement);
        }
    }
}

 

Finally, we need to add an event listener for the page "load" event to initiate the ENV variables substitution process by calling getVariables().

 

window.addEventListener('load',() => {
    //only call this for pages within an actual course
    if (/^\/courses\/[0-9]+/.test(window.location.pathname)){
        getVariables();
    }
});

 

Once this has been added to the active Theme in Canvas, it should now be possible to simulate something similar to Blackboard's template variables. The practical result is that if you include, for example, the variable "ENV.current_user.display_name" in some page content during editing and then save it, it will be replaced by the actual variable value during page loading. Thus, we should be able to construct Canvas content such as:

[image] environment variables in Canvas (code)

and observe it rendered in the Web browser as:

[image] environment variables in Canvas (rendered)

An even better practical example is something like a "certificate of completion" page that we have incorporated into some courses such as our Online Student Orientation. This is how the  implementation of ENV variables appears when editing the page:

[image] environment variables in Certificate page (code)

and this is how it is rendered by a Web browser:

[image] environment variables in Certificate page (rendered)

The possibilities could readily lend themselves to some creative content design.

It should be a relatively straightforward exercise to expand the range of ENV variables that can be substituted with actual values. For instance, if you wanted to reference a course's start and end dates within standard content, you could define "ENV.course_start" and "ENV.course_end" and extract "course.start_at" and "course.end_at" through the API (as I did with "ENV.course_name" from "course.name").

Another note to consider: as defined by the code here and based on the normal operation of a Web browser, you may find that the ENV variables will briefly appear in the content in their literal encoded format (e.g., as "ENV.current_user.display_name") before being replaced by their actual values. I have not been able to devise any method of avoiding or suppressing this, other than to incorporate additional code that hides (using CSS) and then un-hides such content once substitution is complete, so that at least the user only ever observes the actual values. This has been a rather minor obsessive compulsion on my part, but others might not find it as bothersome.

I cannot begin to suspect if anyone would find any of this useful, but on the off chance that someone else might be seeking to replicate Blackboard's template variables, this might offer some solution, or at least a clue towards a more effective solution. Please feel free to adapt or cannibalize this code as you see fit.

3 Comments
DavidTJones
Community Explorer

Thanks for sharing @VVMarshburn.  Both the code and your thought process. A quick search of the community reveals a lot of need for this functionality, including my current institution.

I believe one challenge with this solution (and all use of Javascript) appears to be what happens if the student is using the Canvas app? The combination of Javascript and the app appears to raise some additional considerations (e.g. this)? 

VVMarshburn
Community Explorer
Author

@DavidTJones, thank  you for the feedback. Honestly, I am such a NON-mobile user, that this had not occurred to me. I do not own a smartphone (I know, unheard of), and I use my iPad mainly for a couple games. However, I acknowledge that this is a valid concern, and so I am currently looking into how well this technique and code can be adapted for mobile. Thank you again for the observation.

VVMarshburn
Community Explorer
Author

Thanks to prompting from @DavidTJones, I have revised some of the code described in the original post to make it more compatible with the Canvas mobile app. It seems there may be even less documentation on deploying custom javascript for the mobile platform than what is available for the standard Web platform (which is not entirely copious); but through some experimentation, I was able to deduce some conclusions.

If you are familiar with the technique of adding custom javascript and css to a Canvas theme, you may also be aware that there are separate files maintained for the desktop (Web) application and the mobile app. In this context, one would need to upload or add the following code to the custom javascript for mobile.

Apparently, most of the system default ENV variables for the Web version are not implemented in the mobile app. (Thus far, I have only observed "ENV.COURSE" as default in the mobile app with only the ".id" property.) In particular, "ENV.current_user" does not exist, so any attempt to reference "ENV.current_user.display_name" would not work. To effectively support both Web and mobile, we should just define our own "non-default" variables for use in the script. I ended up specifying "ENV.current_user_fullname" in place of "ENV.current_user.display_name."

 

var envVars = ['ENV.current_user_fullname', 'ENV.courseID', 'ENV.current_user_login', 'ENV.course_name'];

 

The recommendation is to utilize similar variable definition for the Web version as well to ensure consistency.

Based again on observation, the waitUntil() function does not appear to be absolutely necessary for this platform. The mobile app clearly does utilize HTML similar to a Web browser, but it is not a Web browser in terms of design and operation, so we may be able to get away with certain "shortcuts."

The getVariables() function should be revised as follows:

 

async function getVariables() {
    //non-default/derived ENV variables
    ENV.courseID = window.location.pathname.split('/')[2]; //this can be easily extracted from the current URL/path when viewing a course
    ENV.course_name = '';
    ENV.current_user_fullname = ''; //we are replacing ENV.current_user.display_name with our own custom variable, for compatibility with mobile
    ENV.current_user_login = ''; //we sometimes find it useful to have access to the user's login name

    //using API, get current user's profile and assign 'name' property value to ENV.current_user_fullname and 'login_id' property value to ENV.current_user_login
    response = await fetch('/api/v1/users/self/profile');
    profile = await response.json();
    ENV.current_user_fullname = profile.name;
    ENV.current_user_login = profile.login_id.toLowerCase();

    //using API, get course info and assign 'name' property value to ENV.course_name
    response = await fetch('/api/v1/courses/' + ENV.courseID);
    course = await response.json();
    ENV.course_name = course.name;

    //call the function(s) to replace each instance of an ENV variable in a page's content with its value
    replaceInPage();
}

 

Ideally, if our intention is to "synchronize" the code for desktop/Web and mobile app, then we should also adjust the getVariables() function for the desktop platform to replace "ENV.current_user.display_name" with "ENV.current_user_fullname."

Due to how page content is defined in the mobile app, the replaceInPage() function needs to be modified in the following manner (the main difference being identification of page content as the document.body rather than as elements associated with the "ic-Layout-contentWrapper" class):

 

function replaceInPage(){
    var user_content = document.body;
    for (var i=0; i<envVars.length; i++){
        var searchFor = envVars[i];
        var re = new RegExp(searchFor, "g");
        
        //call function to perform actual replacement
        replaceInText(user_content, re, eval(envVars[i]));
    }
}

 

The replaceInText() function should be able to operate without requiring modification. And finally, the event listener for the page "load" event can also remain intact. (See original post for details on both these functions.)

Again, the inclusion of ENV variables in something like a "certificate of completion" page could resemble something like this:

[image] environment variables in Certificate page (code)

and would be rendered in the mobile app thusly:

[image] environment variables in Certificate page (rendered)

Hopefully, this information can serve as a guide for those who may be interested in implementing this type of technique for customizing and personalizing some Canvas content.

Note that due to the way in which assignments, discussions, and quizzes are rendered in the mobile app, the use of these environment variables in such content may produce mixed (or even non-existent) results. The recommendation is to limit their use to Canvas pages content. (It may be possible to discover methods of extending this technique to other mobile app content at some point.)