Guidelines for Using Items API with a Single Page App Architecture

In a typical single page app, views are created and destroyed frequently. If such a view contains an Items API assessment, we'll also want to create and destroy its corresponding itemsApp instance. In this article, we'll cover a basic scenario to achieve this, and an advanced scenario where each session is saved before it's removed.

Throughout the following examples, we'll consider a single page app that uses Items API in Assess rendering mode. Note that when using Items API in Inline rendering mode, it is more natural to use a single instance with addItems().

Basic Scenario

If we only need to display one assessment at a time, we can reuse the same element for each instance. By default, Items API will look for a #learnosity_assess DOM hook element, so we must ensure this element is present in the document before calling LearnosityItems.init().

With the HTML:

<div id="learnosity_assess"></div>

and JavaScript:

var itemsApp = null;

function createItemsApp(initializationObject) {
    destroyItemsApp();

    itemsApp = LearnosityItems.init(initializationObject, {
        readyListener: function() {
            console.log('Items API initialization completed successfully');
        }
    });
}

function destroyItemsApp() {
    if (itemsApp) {
        itemsApp.reset();
        itemsApp = null;
    }
}

Our single page app can call createItemsApp(initializationObject) or destroyItemsApp() at the appropriate times to show a given assessment or remove it.

A caveat with this method is that destroyItemsApp() will discard any unsaved changes to the session. This approach would be suitable for assessments in "preview" or "review" Activity states, since changes are never made in these states, but in cases where saving the session is required, calls to destroyItemsApp() would need to be delayed until saving has completed.

Advanced Scenario

In a more complex scenario, we may want to display multiple Items API assessments at once. To achieve this, we'll need to provide each itemsApp instance with its own DOM hook element. In this example, we'll also save each session automatically before removing it, which will require tracking some additional state about the itemsApp instance.

To see this example in action, visit the Manage Multiple Items API Instances in a Single Page App use case demo on the Learnosity Demos website.

As we'll need to track several pieces of state for each itemsApp instance, we can bundle this into an associated itemsAppStatus object:

function createItemsAppStatus(appId) {
    return {
        appId: appId,
        isActive: false,
        isReady: false,
        testStartEventObserved: false
    };
}

The state we'll need to track is:

  • appId – A string used as the id attribute of the DOM hook element for the corresponding itemsApp instance.
  • isActive – A boolean that indicates whether the DOM hook element is present in the document. This is true after the LearnosityItems.init() method has been called and before the itemsApp.reset() method has been called.
  • isReady – A boolean that indicates whether the readyListener callback has been called and the itemsApp methods are ready to be called.
  • testStartEventObserved – A boolean that indicates whether the "test:start" event has fired and the session is open for changes.

The following functions will be used to create and destroy Items API instances:

function createItemsApp(initializationObject, itemsAppStatus, containerElement) {
    itemsAppStatus.isReady = false;
    itemsAppStatus.testStartEventObserved = false;

    // Create the DOM hook element used by this Items API instance:
    var itemsAppElement = document.createElement('div');
    itemsAppElement.id = itemsAppStatus.appId;
    containerElement.appendChild(itemsAppElement);

    var itemsApp = LearnosityItems.init(initializationObject, itemsAppStatus.appId, {
        readyListener: function() {
            itemsAppStatus.isReady = true;
        }
    });

    itemsApp.once('test:start', function() {
        itemsAppStatus.testStartEventObserved = true;
    });

    itemsAppStatus.isActive = true;

    return itemsApp;
}

function saveAndDestroyItemsApp(itemsApp, itemsAppStatus) {
    if (isSavingRequired(itemsApp, itemsAppStatus)) {
        itemsApp.save();
        itemsApp.on('test:save:success', function() {
            itemsApp.reset();
        });
        itemsApp.on('test:save:error', function(error) {
            console.log('Items API could not save this session', error);
            itemsApp.reset();
        });
    } else if (itemsAppStatus.isActive) {
        itemsApp.reset();
    }

    // Remove the DOM hook element used by this instance:
    var itemsAppElement = document.getElementById(itemsAppStatus.appId);
    if (itemsAppElement) {
        itemsAppElement.parentNode.removeChild(itemsAppElement);
    }

    itemsAppStatus.isActive = false;
    itemsAppStatus.isReady = false;
    itemsAppStatus.testStartEventObserved = false;
}

Finally, since saveAndDestroyItemsApp() needs to know when saving is required, the following function will provide this answer:

function isSavingRequired(itemsApp, itemsAppStatus) {
    if (!itemsAppStatus.isReady) {
        return false;
    }

    var activityState = itemsApp.getActivity().state;
    if (activityState === 'preview' || activityState === 'review') {
        return false;
    }

    return itemsAppStatus.isReady && itemsAppStatus.testStartEventObserved;
}

To create an itemsApp instance, we'll now require:

  • The appId for the new instance. This can be any unique identifier string compatible with the DOM id attribute.
  • An empty container element. A new DOM hook element will be appended into this container element.
  • An itemsAppStatus object to use for tracking state relating to this particular itemsApp instance. We'll pass this object to the createItemsApp() and saveAndDestroyItemsApp() functions.

Putting these together, instances can be created with:

var appId = 'assessment-1';
var containerElement = document.querySelector('.my-assessment-container');
var itemsAppStatus = createItemsAppStatus(appId);
var itemsApp = createItemsApp(initializationObject, itemsAppStatus, containerElement);

and can be destroyed with:

saveAndDestroyItemsApp(itemsApp, itemsAppStatus);

itemsApp = null;
itemsAppStatus = null;

Note that after calling saveAndDestroyItemsApp(), the itemsApp and itemsAppStatus values should not be used. It's a good idea to assign null to these variables to signify that their lifetime has ended.

With the above, multiple assessments can be shown side-by-side, and can be created and destroyed at any time.

Important Notes

Over the lifecycle of an itemsApp instance, there are two cases when its methods are unavailable:

  • When Items API is loading, after the call to LearnosityItems.init() and before the readyListener callback has been called, most itemsApp methods will not exist yet.
  • After the itemsApp.reset() method is called, future calls to itemsApp methods will result in JavaScript errors being thrown (intentionally, since Items API can't fulfill these calls after the instance has been destroyed).

To best manage these limitations, move calls to itemsApp methods into the readyListener callback or parts of the app that are guaranteed to run after the readyListener callback has been called. This applies to all methods with the exceptions of:

  • reset() can be called at any time, even during initialization, and it will detach the Items API instance, remove all event listeners, and restore the DOM to its initial state.
  • Event listener methods on(), off() and once() can be called at any time. It is a good idea to attach event listeners immediately after the call to LearnosityItems.init(), as some events may fire before the readyListener callback is called.

In cases where code may run both before and after the itemsApp instance is ready, we can check itemsAppStatus.isReady to determine how to proceed:

if (itemsAppStatus.isReady) {
    // All methods can be called immediately.
    itemsApp.validateQuestions();
} else {
    // Method calls must be delayed until the itemsApp instance is ready.
    itemsApp.on('app:ready', function() {
        itemsApp.validateQuestions();
    });
}

Lastly, it's important to wait for the "test:start" event before calling the itemsApp.save() or itemsApp.submit() methods.

These methods usually fire "test:save:success", "test:save:error", "test:submit:success" or "test:submit:error" events upon completion, but prior to the "test:start" event – which usually corresponds to when the student clicks the "Start" button to begin an assessment – these events will not fire in response to save() or submit() calls.

This means that if our app calls these methods and relies on the associated events to be notified of their completion, we may end up waiting indefinitely. The solution is to wait for the "test:start" event before calling save() or submit(), or to check isSavingRequired() before calling these methods:

if (isSavingRequired(itemsApp, itemsAppStatus)) {
    itemsApp.submit();
    itemsApp.on('test:submit:success', function() {
        console.log('Test submitted succesfully');
    });
}
Was this article helpful?

Did you arrive here by accident? If so, learn more about Learnosity.