Skip to content

Commit

Permalink
Merge pull request #4520 from christianmemije/develop
Browse files Browse the repository at this point in the history
Update front end docs (WIP)
  • Loading branch information
jonboiser committed Nov 10, 2018
2 parents 5b87570 + d800deb commit 37f7d61
Show file tree
Hide file tree
Showing 17 changed files with 396 additions and 320 deletions.
86 changes: 82 additions & 4 deletions docs/architecture/dataflow/full_stack_data_flow.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
294 changes: 0 additions & 294 deletions docs/architecture/frontend_architecture.rst

This file was deleted.

29 changes: 29 additions & 0 deletions docs/architecture/frontend_architecture/components.rst
@@ -0,0 +1,29 @@
Vue components
==============

We leverage `Vue.js components <https://vuejs.org/guide/components.html>`__ as the primary building blocks for our UI. For general UI development work, this is the most common tool a developer will use. It would be prudent to read through the `Vue.js guide <https://vuejs.org/guide/>`__ thoroughly.

Each component contains HTML with dynamic Vue.js directives, styling which is scoped to that component (written using `SCSS <https://sass-lang.com/>`__), and logic which is also scoped to that component (all code, including that in Vue components should be written using `Bublé compatible ES2015 JavaScript <https://buble.surge.sh/guide/#supported-features>`__).

Components allow us to define new custom tags that encapsulate a piece of self-contained, re-usable UI functionality. When composed together, they form a tree structure of parents and children. Each component has a well-defined interface used by its parent component, made up of `input properties <https://vuejs.org/guide/components.html#Props>`__, `events <https://vuejs.org/guide/components.html#Custom-Events>`__ and `content slots <https://vuejs.org/guide/components.html#Content-Distribution-with-Slots>`__. Components should never reference their parent.

Read through the :doc:`./conventions` for further guidelines on writing components.

Style guide
-----------

Our `style guide <http://kolibridemo.learningequality.org/style_guide>`__ contains reusable components that should be used whenever possible to maintain UI consistency. Adding components to the style guide depends on whether the component is currently used in multiple locations and will likely be used in more locations in the future, matches style guide style, is prop-driven, is properly named.

SVG Icons
---------

`Material Design Icon <https://material.io/tools/icons/>`__ SVGs or local SVGs can be inlined using the `svg-icon-inline-loader <https://github.com/learningequality/svg-icon-inline-loader>`__

.. code-block:: html

<mat-svg category="navigation" name="fullscreen_exit"/>
<file-svg src="./icon.svg"/>

Inlining an SVG allows it to be inserted directly into the outputted HTML. This allows aspects of the icon (e.g. fill) to be styled using CSS.

Attributes (such as vue directives like ``v-if`` and SVG attributes like ``viewbox``) can also be added to the svg tag.
@@ -1,27 +1,38 @@
.. _conventions:

Frontend code conventions
=========================

In general, follow the `style guide <http://kolibridemo.learningequality.org/style_guide>`__ and use built-in components and conventions whenever possible.
Establishing code conventions is important in order to keep a more consistent codebase.

Linting and auto formatting
---------------------------

Many of our conventions are enforced through various linters including `ESLint <https://eslint.org/>`__, `ESLint Vue plugin <https://github.com/vuejs/eslint-plugin-vue>`__, `stylelint <https://stylelint.io/>`__, and `HTMLHint <https://htmlhint.io/>`__. The enforced rules are located in the ``.eslintrc.js``, ``.stylelintrc.js``, and ``.htmlhintrc`` files located at the root of the project. Many of these rules are auto fixed during the Webpack dev server.

We also use `Prettier <https://prettier.io/>`__ to auto format ``.vue``, ``.js``, and ``.scss`` files.

The linting and auto formating is incorporated into the dev server and pre commit hooks, so make sure you've set up `pre-commit` as described in :doc:`/start/getting_started`.

You can also install the appropriate editor plugins for the various linters to see linting warnings/errors inline.

Most syntax conventions are enforced by our linters, so make sure you've set up `pre-commit` as described in :doc:`../start/getting_started`.

Vue.js components
-----------------

- Make sure to follow the official `Vue.js style guide <https://vuejs.org/v2/style-guide/>`__ when creating Vue components.
- Keep components stateless and declarative as much as possible
- For simple components, make *SomeComonent.vue*. For more complex components, make *SomeComponent/index.vue* and add private sub-components
- All user-visible app text should be internationalized. See :doc:`i18n` for details
- All user-visible app text should be internationalized. See :doc:`/references/i18n` for details
- Avoid direct DOM references and Vue component "lifecycle events" except in special cases
- Props, slots, and Vuex state/getters for communicating down the view hierarchy
- Events and Vuex actions for communicating up the view hierarchy
- If possible, use `<template/>` for conditionals to avoid extra unnecessary nested elements.


Styling anti-patterns
---------------------

- **Adding unnecessary new rules** - whenever possible, delete code to fix issues
- **Unscoped styles** - if absolutely necessary, use `deep selectors <https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors>`__ to style component children
- **Unscoped styles** - if absolutely necessary, use `deep selectors <https://vue-loader.vuejs.org/guide/scoped-css.html#deep-selectors>`__ to style component children. SCSS supports ``/deep/``
- **Classes referenced in javascript** - if absolutely necessary, use `ref <https://vuejs.org/v2/api/#ref>`__ instead (also an anti-pattern)
- **References by ID** - use a ``class`` instead
- **HTML tag selectors** - define a ``class`` instead
Expand All @@ -30,5 +41,5 @@ Styling anti-patterns
- **Nested selectors** - make a sub-component instead (more reading `here <https://csswizardry.com/2012/05/keep-your-css-selectors-short/>`__ and `here <http://thesassway.com/beginner/the-inception-rule>`__)
- **Dynamically-generated class names** - avoid patterns which fail the `grep test <http://jamie-wong.com/2013/07/12/grep-test/>`__
- **Complex pre-processor functionality** - use Vue `computed styles <https://vuejs.org/v2/guide/class-and-style.html>`__ instead
- **Hard-coded values** - rely on core themes and components
- **Hard-coded values** - rely on variables defined in the core theme
- **Left or right alignment on user-generated text** - use ``dir="auto"`` instead for RTL support
69 changes: 69 additions & 0 deletions docs/architecture/frontend_architecture/core.rst
@@ -0,0 +1,69 @@
Shared core functionality
=========================


Kolibri provides a set of shared "core" functionality – including components, styles, and helper logic, and libraries – which can be re-used across apps and plugins.

JS libraries and Vue components
-------------------------------

The following libraries and components are available globally, in all module code:

- ``vue`` - the Vue.js object
- ``vuex`` - the Vuex object
- ``logging`` - our wrapper around the `loglevel logging module <https://github.com/pimterry/loglevel>`__
- ``CoreBase`` - a shared base Vue.js component (*CoreBase.vue*)

And **many** others. The complete specification for commonly shared modules can be found in `kolibri/core/assets/src/core-app/apiSpec.js`. This object defines which modules are imported into the core object. These can then be imported throughout the codebase - e.g.:

.. code-block:: javascript
import Vue from 'kolibri.lib.vue';
import CoreBase from 'kolibri.coreVue.components.CoreBase';
Adding additional globally-available objects is relatively straightforward due to the `plugin and webpack build system </pipeline/frontend_build_pipeline>`__.

To expose something in the core app, add the module to the object in `apiSpec.js`, scoping it to the appropriate property for better organization - e.g.:

.. code-block:: javascript
components: {
CoreTable,
},
utils: {
navComponents,
},
These modules would now be available for import anywhere with the following statements:

.. code-block:: javascript
import CoreTable from 'kolibri.coreVue.components.CoreTable';
import navComponents from 'kolibri.utils.navComponents';
.. note::
In order to avoid bloating the core api, only add modules that need to be used in multiple plugins.

Styling
-------

To help enforce style guide specs, we provide global variables that can be used throughout the codebase. This requires including ``@import '~kolibri.styles.definitions';`` within a SCSS file or a component's ``<style>`` block. This exposes all variables in ``core-theme.scss`` and ``definitions.scss``.

Bootstrapped data
-----------------

The ``kolibriGlobal`` object is also used to bootstrap data into the JS app, rather than making unnecessary API requests.

For example, we currently embellish the ``kolibriGlobal`` object with a ``urls`` object. This is defined by `Django JS Reverse <https://github.com/ierror/django-js-reverse>`__ and exposes Django URLs on the client side. This will primarily be used for accessing API Urls for synchronizing with the REST API. See the Django JS Reverse documentation for details on invoking the Url.

Additional functionality
------------------------

These methods are also publicly exposed methods of the core app:

.. code-block:: javascript
kolibriGlobal.register_kolibri_module_async // Register a Kolibri module for asynchronous loading.
kolibriGlobal.register_kolibri_module_sync // Register a Kolibri module once it has loaded.
kolibriGlobal.stopListening // Unbind an event/callback pair from triggering.
kolibriGlobal.emit // Emit an event, with optional args.
12 changes: 12 additions & 0 deletions docs/architecture/frontend_architecture/dependencies.rst
@@ -0,0 +1,12 @@
Adding dependencies
===================

Dependencies are tracked using ``yarn`` - `see the docs here <https://yarnpkg.com/en/docs/>`__.

We distinguish development dependencies from runtime dependencies, and these should be installed as such using ``yarn add --dev [dep]`` or ``yarn add [dep]``, respectively. Your new dependency should now be recorded in *package.json*, and all of its dependencies should be recorded in *yarn.lock*.

Individual plugins can also have their own package.json and yarn.lock for their own dependencies. Running ``yarn install`` will also install all the dependencies for each activated plugin (inside a node_modules folder inside the plugin itself). These dependencies will only be available to that plugin at build time. Dependencies for individual plugins should be added from within the root directory of that particular plugin.

To assist in tracking the source of bloat in our codebase, the command ``yarn run bundle-stats`` is available to give a full readout of the size that uglified packages take up in the final Javascript code.

In addition, a plugin can have its own webpack.config.js for plugin specific webpack configuration (loaders, plugins, etc.). These options will be merged with the base options using ``webpack-merge``.
18 changes: 18 additions & 0 deletions docs/architecture/frontend_architecture/index.rst
@@ -0,0 +1,18 @@
Frontend architecture
=====================

.. toctree::

single_page_apps
layout
core
components
conventions
vuex
dependencies
unit_testing


apps
shared
all these contain
55 changes: 55 additions & 0 deletions docs/architecture/frontend_architecture/layout.rst
@@ -0,0 +1,55 @@
Layout of frontend code
=======================

Frontend code and assets are generally contained in one of two places: either in one of the plugin subdirectories (under *kolibri/plugins*) or in *kolibri/core*, which contains code shared across all plugins as described below.

Within these directories, there should be an *assets* directory with *src* and *test* under it. Most assets will go in *src*, and tests for the components will go in *test*.

For example:

.. code-block:: none
kolibri/
core/ # core (shared) items
assets/
src/
CoreBase.vue # global base template, used by plugins
CoreModal.vue # example of another shared component
core-global.scss # globally defined styles, included in head
core-theme.scss # style variable values
font-noto-sans.css # embedded font
test/
... # tests for core assets
plugins/
learn # learn plugin
assets/
src/
views/
LearnIndex.vue # root view
SomePage.vue # top-level client-side page
AnotherPage/ # top-level client-side page
index.vue
Child.vue # child component used only by parent
Shared.vue # shared across this plugin
app.js # instantiate learn app on client-side
router.js
store.js
test/
app.js
management/
assets/
src/
views/UserPage.vue # nested-view
views/ManagementIndex.vue # root view
app.js # instantiate mgmt app on client-side
test/
app.js
In the example above, the *views/AnotherPage/index.vue* file in *learn* can use other assets in the same directory (such as *Child.vue*), components in *views* (such as *Shared.vue*), and assets in core (such as variables in *core-theme.scss*). However it cannot use files in other plugin directories (such as *management*).

.. note::

For many development scenarios, only files in these directories need to be touched.

There is also a lot of logic and configuration relevant to frontend code loading, parsing, testing, and linting. This includes webpack, NPM, and integration with the plugin system. This is somewhat scattered, and includes logic in *frontend_build/...*, *package.json*, *kolibri/core/webpack/...*, and other locations. Much of this functionality is described in other sections of the docs (such as :doc:`/pipeline/frontend_build_pipeline`), but it can take some time to understand how it all hangs together.
74 changes: 74 additions & 0 deletions docs/architecture/frontend_architecture/single_page_apps.rst
@@ -0,0 +1,74 @@
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 :doc:`/architecture/plugins`), 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 ``kolibriGlobal``. 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 :doc:`/pipeline/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.

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 accepts a json file defining the content types that it renders.

.. automodule:: kolibri.core.content.hooks
:members:
:noindex:

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 ``defaultFile``, ``files``, ``supplementaryFiles``, and ``thumbnailFiles`` props, defining the files associated with the piece of content. These can be automatically mixed into a content renderer component definition using the content renderer mixin.

.. code-block:: javascript
import contentRendererMixin from 'kolibri.coreVue.mixins.contentRendererMixin';
{
mixins: [contentRendererMixin],
};
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.

.. code-block:: javascript
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.

.. code-block:: javascript
import contentRendererMixin from 'kolibri.coreVue.mixins.contentRendererMixin';
{
mixins: [contentRendererMixin],
methods: {
checkAnswer() {
return {
correct: true,
answerState: {
answer: 81,
working: '3^2 = 3 * 3',
},
simpleAnswer: '81',
};
},
},
};

0 comments on commit 37f7d61

Please sign in to comment.