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 theid
attribute of the DOM hook element for the correspondingitemsApp
instance.isActive
– A boolean that indicates whether the DOM hook element is present in the document. This istrue
after theLearnosityItems.init()
method has been called and before theitemsApp.reset()
method has been called.isReady
– A boolean that indicates whether thereadyListener
callback has been called and theitemsApp
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 DOMid
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 particularitemsApp
instance. We'll pass this object to thecreateItemsApp()
andsaveAndDestroyItemsApp()
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 thereadyListener
callback has been called, mostitemsApp
methods will not exist yet. - After the
itemsApp.reset()
method is called, future calls toitemsApp
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()
andonce()
can be called at any time. It is a good idea to attach event listeners immediately after the call toLearnosityItems.init()
, as some events may fire before thereadyListener
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');
});
}