The Tui framework uses a fairly standard ESLint configuration to enable developers with make programming decisions that are both considerate of browser performance and follow a general style approach. There are escape hatches when needed, as there are sometimes exceptions to rules.
We use ES Modules as our module system and Webpack as our module bundler, with some custom extensions to allow for cross-bundle module usage. Each Vue component is an ES module.
Locations and importing
All modules (including JS files and Vue components) must be in a specified subdirectory of the src folder of a Tui component in the client directory.
The supported subdirectories are js, pages, and components. Modules within the js folder are importable as
component_name/myfile, whereas the other two require their folder name in the import path, e.g.
/components/Example. Omit the file extension when importing.
Relative imports may be used for JS files, but absolute imports starting with the component name must always be used for Vue components, otherwise the import will not pick up any overrides from the theme.
Use the ES import syntax for importing files:
totara_dashboard. If it is a static dependency (i.e. you know what component you need ahead of time), you can add it to
dependencies in tui.json, e.g.
"dependencies": ["totara_job"] and that bundle will be loaded alongside the bundle for your Tui component.
If you don't know what component you want ahead of time, for example if its name is returned by an API call, or want to delay loading the bundle it is contained in until it is actually used, there are a couple of options.
You can call the async function
tui.import() with the name of the module and it will resolve with the module once it has been loaded. This will return all exports of the module. To get the default export you can use
tui.defaultExport() on the result, or
.default if it is an ES module.
For Vue components, the preferred option is to use
tui.asyncComponent(). This makes use of Vue's async components feature to immediately return an object that can be rendered as a component immediately, rendering a loading spinner until the real component is loaded.
tui.asyncComponent() will also automatically load language strings for the component.
Another option is to call the async function
tui.loadComponent(), which returns a promise resolving to the component.
If your Tui component has a dependency on another Tui component (whether static or async), you should also specify a dependency in the corresponding PHP Totara plugin.
We provide a library of utility functions as part of the Tui core code.
They are well commented so the best place to get the full details is in the code, but here is an overview of the major ones:
- tui/util: Array helpers like
groupBy(), object helpers like
structuralDeepClone(), and function helpers like
totaraUrl(), which generates a URL to a page on Totara, can also be imported from here
- tui/config: General page configuration info like the web root, session key, and current theme
- tui/notifications: Show a notification toast message for a successful action
- tui/theme: Functions for interacting with the theme, e.g. getting CSS variable values
- tui/errors: Error display code
- tui/accessibility: Accessibility helpers
- tui/pending: Signal to behat that we are waiting for something
- tui/dom/focus: Control focus
- tui/dom/position: Get the position of elements on the page
- tui/dom/transitions: Wait for a transition
In general, the recommended way to write async code is using the ES6 async/await feature. You can also use Promises, which async/await use under the hood.
The use of callbacks for asynchronous functionality is strongly discouraged in favour of async/await and Promises as it makes for much more maintainable code.
Loading data in components
Data loading in components is done over GraphQL. You can use the tui/components/loader/Loader component to display a loading spinner while content is loading.
It's recommended to base the rendering state on whether the query result is populated yet rather than
$apollo.loading will be false if an error occurs, and will be true again if the query re-fetches for any reason. However, you can combine this with checking
$apollo.loading in the condition too so that the loader doesn't stay visible if the query fails.
Tips and known limitations
IE 11 limitations
There are some limitations to be aware of when writing code that needs to work with IE 11.
Aside from JS APIs, the following JS language features do not work in either IE:
for...ofcan not be polyfilled in IE 11 in a way that is both performant and standards-compliant
- Generators do technically work but will be flagged by the linter as they perform poorly in IE 11
If you are using Totara 13-15 (which supports Edge Legacy 16-18), the following language features cannot be used:
- Object spread is not supported by Edge Legacy. Array spread and parameter spread are fine
These JS APIs are supported but have some pitfalls:
Object.keys()with a non-object argument throws an exception in IE, so make sure to only call it with an object.
- Promise is supported via Polyfill in IE 11, but due to the aforementioned lack of micro-task support, it may not behave exactly as in other browsers, which can expose race condition bugs with user events that don't happen in other browsers. However, this is rare and the solution is usually to improve the robustness of your code as there is usually an underlying problem if you run into this.
IE 11 lacks support for executing JS in micro-tasks, which can result in slightly degraded performance of asynchronous code and updates to Vue components being delayed until after native events are processed. This is a platform-level feature that is not possible to polyfill or emulate.