UNPKG

react-cmi5

Version:

React wrapper component for an xapi/cmi5 assignable unit @see https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#au_requirements

240 lines (215 loc) 7.04 kB
import React, { Component } from 'react'; const CMI_STATUS = { NONE: 0, LOADING: 1, READY: 2, RESULT_SENT: 3, ERROR: -1 } /** * A reusable wrapper Component that handles cmi initialization * for a question (cmi5 assignable unit), which should be a child component. * * Cmi5AssignableUnit will inject the following props to it's child: * * passed: * A function for the assignable unit to call when the question was answered correctly. * For arguments, accepts either a single normalized score (0-1) value * or an object that's a valid XAPI result 'score' * @see https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#Score * * failed: * A function for the assignable unit to call when the question was answered incorrectly. * For arguments, accepts either a single normalized score (0-1) value * or an object that's a valid XAPI result 'score' * @see https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#Score * * cmi: * the Cmi5 instance, which can be used directly * @see ../public/cm5.js * * The actions for 'passed' and 'failed' are injected to child components * to provide a simple/safe way to submit results: this wrapped will handle * the actual initialization of cmi5, * and if either of the inject 'passed' or 'failed' functions is called * before cmi5 is ready, it will store the result and submit when ready. */ class Cmi5AU extends Component { constructor(props) { super(props) this.state = { cmiStatus: CMI_STATUS.NONE, loading: true, answered: false } this.passed = this.passed.bind(this) this.failed = this.failed.bind(this) this.completed = this.completed.bind(this) this.terminate = this.terminate.bind(this) this._submitScore = this._submitScore.bind(this) this._execCmiOrQueue = this._execCmiOrQueue.bind(this) } /** * A function for the assignable unit to call when the user passes an assessment (e.g. answers a question correctly. * * @param {number|XAPI Score} score * * For arguments, accepts either a single normalized score (0.0-1.0) value * or an object that's a valid XAPI Result Score (https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#2451-score) * * @see https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#verbs_failed} score */ passed(score) { this._submitScore(score, true) } /** * A function for the assignable unit to call when the user fails an assessment (e.g. answers a question incorrectly. * * @param {number|XAPI Score} score * * For arguments, accepts either a single normalized score (0.0-1.0) value * or an object that's a valid XAPI Result Score (https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#2451-score) * * @see https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#verbs_failed} score */ failed(score) { this._submitScore(score, false) } /** * A function for the assignable unit to call, * when it's a non-assessment type resource (no score) * and the user has completed it, e.g. finished watching a video. * * @see https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#verbs_completed */ completed() { this._execCmiOrQueue(() => { this.state.cmi.completed() }) } /** * Required function the assignable unit MUST call, * to signal to the LMS (container that launched the assignable unit) * that the user's session is complete (no additional xapi statements will be sent) * @see https://github.com/AICC/CMI-5_Spec_Current/blob/quartz/cmi5_spec.md#verbs_terminated */ terminate() { this._execCmiOrQueue(() => { this.state.cmi.terminate() }) } /** * @private * submit a score if cmi is ready, * or if cmi is not ready, store the action for later execution. */ _submitScore(result, isPassing) { const score = isNaN(Number(result))? result: { scaled: Number(result) } this._execCmiOrQueue(() => { if(isPassing) { this.state.cmi.passed(score) } else { this.state.cmi.failed(score) } }) this.setState({ ...this.state, answered: true }) } /** * @private * Execute an action (generally a cmi call) if cmi is ready, * or if cmi is not ready, store the action for later execution. * * @param {*} action - a function to call if/when cmi is ready */ _execCmiOrQueue(action) { if(this.state.cmiStatus !== CMI_STATUS.READY) { // save the action to submit when cmi is ready this.setState({ ...this.state, actionsPendingCmiReady: Array.isArray(this.state.actionsPendingCmiReady)? [...this.state.actionsPendingCmiReady, action] : [action] }) return } try { action() } catch(cmiErr) { console.error(`cmi action failed: ${cmiErr.message}\n${cmiErr.stack}`) } } componentDidMount() { try { const Cmi5 = window.Cmi5 const url = this.props.url || window.location.href console.log(`launch cmi with url ${url}`) const cmi = new Cmi5(url) cmi.start((err, result) => { if(err) { this.setState({ ...this.state, cmiStatus: CMI_STATUS.ERROR, cmiMessage: err.message }) console.error(err) return } this.setState({ ...this.state, cmiStatus: CMI_STATUS.READY, loading: false }) }) this.setState({ ...this.state, cmi: cmi, }) } catch(errInit) { console.error(`error loading cmi: ${errInit.message}`) this.setState({ ...this.state, cmiStatus: CMI_STATUS.NONE, cmiMessage: errInit.message }) } } render() { console.log(`render state.cmiStatus=${this.state.cmiStatus}`) switch(this.state.cmiStatus) { case CMI_STATUS.READY: if(Array.isArray(this.state.actionsPendingCmiReady)) { this.actionsPendingCmiReady.forEach(a => a()) } break case CMI_STATUS.ERROR: console.error(`cmi error: ${this.state.cmiMessage}`) break default: console.log(`cmi status updated to ${this.state.cmiStatus}`) } const { children } = this.props const assignableUnit = React.Children.map(children, child => React.cloneElement(child, { cmi: this.state.cmi, terminate: this.terminate, // action that MUST be called to signal end of the session passed: this.passed, // action for child to call on passed an assessment (with score) failed: this.failed, // action for child to call on failed an assessment (with score) completed: this.completed, // action for child to call on completion of a non-assessment resource, e.g. finished watching video loading: this.state.loading, answered: this.state.answered } ) ) return( <div>{assignableUnit}</div> ) } } export default Cmi5AU