How can I implement support for resuming in an H5P content type?

Implementing H5P’s resume (or “save content state”) feature is pretty simple for developers. Whenever the core of H5P wants to store the current state of a content type instance, it will try to call a function named getCurrentState and expects the function to return anything serializable that you might want to store.

When the user opens some H5P content and should resume, then H5P core will pass the previous state as a property of the constructor’s third argument. It is commonly named extras or contentData in existing content types. You will receive what you stored and can use that information to restore the state as you need it in your content type.

So, in order to make sure that your state data can be saved, you only need to make sure that there is an exposed function named appropriately that does exactly that. For instance:

/**
 * Answer H5P core call to return the current state.
 *
 * @returns {object} Current state.
 */
getCurrentState() {
  return {
    answersChecked: [1, 2, 3, 5, 8, 13, 21],
    somethingNested: {
      foo: 'FOO',
      bar: 'BAR'
    },
    somethingElse: true
  };
}

In practice, while you could return anything, you should always return an object even though you currently may only need one single number or maybe an array. You may not need to store much now, but if you ever do later on as your content type evolves, you would need to add extra code to distinguish whether the previous state that you will receive still is a boolean / number / "array" / string or already an object.

/**
 * @class
 * @param {object} params Parameters passed by the editor.
 * @param {number} contentId Content's id.
 * @param {object} [extras={}] Saved state, metadata, etc.
 */
constructor(params, contentId, extras = {}) {
  /*
   * `extras.previousState` should either be nullish if there is
   * no previous state that needs to be re-created or it
   * will be the return value of your `getCurrentState` function.
   */
}

It is important to note, that some H5P integrations (like H5P.com) may offer some extra functionality that requires some additional handling. For instance. H5P.com features a button that allows to reset the exercise. That button should only appear if there in fact is something to be reset, and this is checked by looking at the previous state. In turn that means that the getCurrentState function should not return anything if the user has not taken some relevant action that could be reset. More precisely, if you pass the return value to the function H5P.isEmpty, you should receiveĀ true. But not returning anything from getCurrentState at all if not required just feels cleaner.

For instance, if you implemented a function shouldUserBeAbleToReset that returned false if the user has not yet done anything that would make a reset button useful and true otherwise, your getCurrentState function could look similar to this:

/**
 * Answer H5P core call to return the current state.
 *
 * @returns {object} Current state.
 */
getCurrentState() {
  if (!shouldUserBeAbleToReset()) {
    return;
  }

  return {
    answersChecked: [1, 2, 3, 5, 8, 13, 21],
    somethingNested: {
      foo: 'FOO',
      bar: 'BAR'
    },
    somethingElse: true
  };
}

Some extra behavior that is expected but is not listed anywhere in the official documentation (when writing these lines): When your content type supports the resetTask function, whenever that is called, it should bring your content into its initial state – so the user has not answered anything and getCurrentState could return undefined. However, when you now refresh the page, the previous state from before the reset would be restored, as returning undefined leaves it untouched. So, whenresetTask is done and the user has not taken any action, your getCurrentState function should return {}. That will overwrite existing states in the database with an empty object.

How do I add support to compound content types?

If you ever create a compound content type that holds H5P subcontents, your compound content type can collect all the subcontents’ states by calling their getCurrentState function (if they have one) and pass the returned values (or undefined) as part of the compound content type’s state. H5P core does not communicate with the subcontents directly. You will need to fill that gap.

The other way round, when you receive the previous state when instantiating your compound content type, you need to send the subcontens’ previous states to the right recipients. Normally, the newRunnable function of H5P core is used to instantiate subcontent. That function expects a previousState property inside the 5th argument. Its value needs to be the state information that you received from getCurrentState like so:

const subcontentInstance = H5P.newRunnable(
  library,
  contentId,
  H5P.jQuery(element),
  skipResize,
  { previousState: { ... } }
);