Single-page Apps

The Kolibri frontend is made of a few high-level “app” plugins, which are single-page JS applications (conventionally app.js) with their own base URL and a single root Vue.js component. Examples of apps are ‘Learn’ and ‘User Management’. Apps are independent of each other, and can only reference components and styles from within themselves and from core.

Each app is implemented as a Kolibri plugin (see Kolibri plugin architecture), and is defined in a subdirectory of kolibri/plugins.

On the Server-side, the kolibri_plugin.py file describes most of the configuration for the single-page app. In particular, this includes the base Django HTML template to return (with an empty <body>), the URL at which the app is exposed, and the javascript entry file which is run on load.

On the client-side, the app creates a single KolibriModule object in the entry file (conventionally app.js) and registers this with the core app, a global variable called kolibriCoreAppGlobal. The Kolibri Module then mounts single root component to the HTML returned by the server, which recursively contains all additional components, html and logic.

Defining a new Kolibri module

Note

This section is mostly relevant if you are creating a new app or plugin. If you are just creating new components, you don’t need to do this.

A Kolibri Module is initially defined in Python by sub-classing the WebpackBundleHook class (in kolibri.core.webpack.hooks). The hook defines the JS entry point (conventionally called app.js) where the KolibriModule subclass is instantiated, and where events and callbacks on the module are registered. These are defined in the events and once properties. Each defines key-value pairs of the name of an event, and the name of the method on the KolibriModule object. When these events are triggered on the Kolibri core JavaScript app, these callbacks will be called. (If the KolibriModule is registered for asynchronous loading, the Kolibri Module will first be loaded, and then the callbacks called when it is ready. See Frontend build pipeline for more information.)

All apps should extend the KolibriModule class found in kolibri/core/assets/src/kolibri_module.js.

The ready method will be automatically executed once the Module is loaded and registered with the Kolibri Core App. By convention, JavaScript is injected into the served HTML after the <rootvue> tag, meaning that this tag should be available when the ready method is called, and the root component (conventionally in vue/index.vue) can be mounted here.

Creating a side nav entry

If you want to expose your new single page app as a top level navigation item in the sidebar nav, then it is necessary to create a nav item in your plugin. This is implemented as a hook, which is a combination of the WebpackBundleHook and a navigation hook. So it allows the creation of a navigation item frontend bundle, and signalling that this should be included as a navigation item. Here is an example of it in use.

from kolibri.core.hooks import NavigationHook
from kolibri.plugins.hooks import register_hook

@register_hook
class ExampleNavItem(NavigationHook):
    bundle_id = "side_nav"

For more information on using bundle_id and connecting it to the relevant Javascript entry point read the documentation on the Frontend build pipeline. The entry point for the nav item should minimally do the following:

<template>

  <CoreMenuOption
    :label="$tr('label')"
    :link="url"
    icon="learn"
  />

</template>


<script>

  import CoreMenuOption from 'kolibri.coreVue.components.CoreMenuOption';
  import navComponents from 'kolibri.utils.navComponents';
  import urls from 'kolibri.urls';

  const component = {
    name: 'ExampleSideNavEntry',
    components: {
      CoreMenuOption,
    },
    computed: {
      url() {
        return urls['kolibri:kolibri.plugins.example:example']();
      },
    },
    priority: 5,
    $tr: {
      label: 'Example',
    },
  };

  navComponents.register(component);

  export default component;

</script>

This will create a navigation component which will be registered to appear in the navigation side bar.

Content renderers

A special kind of Kolibri Module is dedicated to rendering particular content types. All content renderers should extend the ContentRendererModule class found in kolibri/core/assets/src/content_renderer_module.js. In addition, rather than subclassing the WebpackBundleHook class, content renderers should be defined in the Python code using the ContentRendererHook class defined in kolibri.content.hooks. In addition to the standard options for the WebpackBundleHook, the ContentRendererHook also requires a presets tuple listing the format presets that it will render.

Kolibri Content hooks

Hooks for managing the display and rendering of content.

class kolibri.core.content.hooks.ContentNodeDisplayHook[source]

A hook that registers a capability of a plugin to provide a user interface for a content node. When subclassed, this hook should expose a method that accepts a ContentNode instance as an argument, and returns a URL where the interface to interacting with that node for the user is exposed. If this plugin cannot produce an interface for this particular content node then it may return None.

class kolibri.core.content.hooks.ContentRendererHook[source]

An inheritable hook that allows special behaviour for a frontend module that defines a content renderer.

render_to_page_load_async_html()[source]

Generates script tag containing Javascript to register a content renderer.

Returns

HTML of a script tag to insert into a page.

The ContentRendererModule class has one required property getRendererComponent which should return a Vue component that wraps the content rendering code. This component will be passed files, file, itemData, preset, itemId, answerState, allowHints, extraFields, interactive, lang, showCorrectAnswer, defaultItemPreset, availableFiles, defaultFile, supplementaryFiles, thumbnailFiles, contentDirection, and contentIsRtl props, defining the files associated with the piece of content, and other required data for rendering. These will be automatically mixed into any content renderer component definition when loaded. For more details of these props see the Content Renderer documentation.

In order to log data about users viewing content, the component should emit startTracking, updateProgress, and stopTracking events, using the Vue $emit method. startTracking and stopTracking are emitted without any arguments, whereas updateProgress should be emitted with a single value between 0 and 1 representing the current proportion of progress on the content.

this.$emit('startTracking');
this.$emit('stopTracking');
this.$emit('updateProgress', 0.25);

For content that has assessment functionality three additional props will be passed: itemId, answerState, and showCorrectAnswer. itemId is a unique identifier for that content for a particular question in the assessment, answerState is passed to prefill an answer (one that has been previously given on an exam, or for a coach to preview a learner’s given answers), showCorrectAnswer is a Boolean that determines if the correct answer for the question should be automatically prefilled without user input - this will only be activated in the case that answerState is falsy - if the renderer is asked to fill in the correct answer, but is unable to do so, it should emit an answerUnavailable event.

The answer renderer should also define a checkAnswer method in its component methods, this method should return an object with the following keys: correct, answerState, and simpleAnswer - describing the correctness, an object describing the answer that can be used to reconstruct it within the renderer, and a simple, human readable answer. If no valid answer is given, null should be returned. In addition to the base content renderer events, assessment items can also emit a hintTaken event to indicate that the user has taken a hint in the assessment, an itemError event to indicate that there has been an error in rendering the requested question corresponding to the itemId, and an interaction event that indicates a user has interacted with the assessment.

{
  methods: {
    checkAnswer() {
      return {
        correct: true,
        answerState: {
          answer: 81,
          working: '3^2 = 3 * 3',
        },
        simpleAnswer: '81',
      };
    },
  },
};