This document describes the frontend coding guidelines
Please remember the TAO software is an 15+ years old software. Yes. Some headers in the source code can attest it :
* Copyright (c) 2002-2008 (original work) Public Research Centre Henri Tudor & University of Luxembourg (under the project TAO & TAO2);
* 2008-2010 (update and modification) Deutsche Institut für Internationale Pädagogische Forschung (under the project TAO-TRANSFER);
* 2009-2012 (update and modification) Public Research Centre Henri Tudor (under the project TAO-SUSTAIN & TAO-DEV);
* 2013-2014 (update and modification) Open Assessment Technologies SA;
And the software has also crossed the years by evolving, step by step. Some pieces of code being developed at some point, some other later. Some parts are very good, some other aren't.
Keeping this in mind, you'll always see exceptions to the rules described in this document. So takes those rules as guidelines, as a goal to reach. When doing something new, please follow them strictly, but when fixing a bug, sometimes it's worth refactoring, sometimes it isn't.
Every file MUST contain the copyright and license header with the correct year. Here is the default open source header :
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2019-2022 Open Assessment Technologies SA;
*/
We use a subset of the jsdoc format for the JavaScript code documentation.
Mostly we document :
- functions and method parameters
- complex types
- return values
- thrown errors
- fired events
- callbacks/lambda
The documentation doesn't target tools, it targets humans. It is important to clearly and precisely document APIs, but it's even more important to communicate the intents.
For example :
/**
* A fooBar provides you the foo.
* @type {fooBar}
*/
const fooBar = {
/**
* This is how you get foo
* @param {String[]} ids - the foo identifiers
* @param {Object} [options]
* @param {Boolean} [options.force = false] - force the foo
* @returns {Promise<Number>} resolves with the number of updated foos
* @fires fooBar#foo once the foo has foo the foo event is triggered
* @throws {TypeError} if the parameters are invalid
*/
foo(ids, options) {
//...
}
};
The golden rule is the consistency
Please configure your IDE or development editor to support :
- JavaScript ES5 and ES2015+ style
- CSS, SASS, HTML, JSON and Handlebars templates, etc.
- ESLint, make sure IDE uses
.eslintrc.js
which imports config from eslint-config-tao - Prettier, if
prettier.config.js
exists in repository, configure IDE to use, this file imports config from prettier-config. You should setup this configuration as a fallback optionpackage.json
config example :
{
"prettier": "@oat-sa/prettier-config",
"devDependencies": {
"@oat-sa/eslint-config-tao": "^1.1.1",
"@oat-sa/prettier-config": "^0.1.1",
"eslint": "^8.25.0",
"eslint-plugin-es": "^4.1.0",
"eslint-plugin-jsdoc": "^39.3.6",
"prettier": "^2.7.1"
}
}
- EditorConfig, using this configuration
- Code should be consistent and easy to understand and self descriptive.
- Writing comments when it is necessary is highly appreciated. Comments shouldn't be redundant with the code itself (clear code document itself) but provide additional and useful information.
- Variable, function, method names should reflect the intent in a clear way.
- Variable definitions should be separated always by new lines to help readability.
- Variable should be defined using it's own statement (no comma after the declaration)
- Use 4 spaces for indents
- No more than one blank line
- No ASCII art within the source code
- Use single quotes for string literals
- Brace style 1TBS
- Try to avoid using ternary operator in complex cases (or don't use at all?)
- Always use
===
instead of==
- Variable names for jQuery elements have to start with
$
- No underscore to private variables functions
If an extension or a project doesn't yet support ES2015+, the following rules apply :
- always in strict mode :
'use strict';
in the highest scope - ensure to always code in a non global lexical scope (it's the case for AMD or CommonJS, otherwise use IIFE)
- named function expressions for methods :
{ method : function method() }
- named callbacks for easier debugging :
on('click', function buttonOkClickHandler(e){
- references to the lexical scope are made using the
self
variable name (for consistency) - hoisting should be reflected by variable declaration, ie.
var
on top. Promise
is available to manage asynchronous flow.
Allowed features from the ES2015 specification :
- All features from previous specifications (ES3, ES5)
const
andlet
.Object
andArray
new built-in (Objet.assign
,Array.from
,Array.of
, etc.)String
new built-in (String.includes
,String.startWith
, etc.)Promise
Map
,Set
(includingWeakMap
andWeakSet
)- Typed arrays
- default function parameters
- rest parameters
- spread syntax on objects
- shorthand object notation
- for ... of
- template literals
- destructuring
- arrow function
- import/export only when the bundler supports it (library, side projects)
The following coding rules apply on ES2015 code :
- no
use strict
anymore in modules, but it needs to be kept outside ES2015 modules. - Named function expressions can be replaced by the shorthand object notation
{ method(){ } }
- Do not use
class
, always prefer composition over inheritance. However there are a few use casesclass
would be allowed, for example to extend DOM prototypes, likeError
to create new error types. - Try to use
const
by default (immutable) andlet
only for mutable variables (counters). - Be careful with destructuring, this can create code difficult to read. A reviewer can ask to rewrite a destructred assignment if the produced code is cryptic.
- Use arrow functions for lambda to avoid unnecessary lexical scopes, but avoid them for top-level factories or pure function definitions.
- Use template literals instead of string concatenation.
- Use destructuring and default parameters for method's
options
parameter :function({label = "", num = 0} = {}) { }
- Use rest notation
(...args)=>{console.log(args)}
instead ofarguments
- Do not use
!important
, never. - Do not use inline style, ie
style="font-size:16px"
, never. - When grouping selectors, keep individual selectors on a single line.
- Include one space before the opening brace of declaration blocks for legibility.
- Each declaration should appear on its own line for more accurate error reporting.
- End all declarations with a semi-colon.
- Avoid specifying units for zero values.
- Keep classes lowercase and use hyphens (not underscores or camelCase). Dashes serve as natural breaks in related class.
- Use class names that describe the purpose of the element, rather than the presentation of the element : do not use classes like
.center
or.column
nor.button
but instead.user-profile
,.highlighted-stimulus
ornotification-area
. - Except for base styles (reset, normalize, base and font) every rule must be scoped, by the class name of the component or a root component.
- Try to avoid IDs in selector as much as possible and global tag selectors. Instead try to always scope the selection.
- Selection must be done by following the semantic of the DOM ie.
.actions > button
or.actions > [role=button]
instead of.actions > .btn
- Place media queries as close to their relevant rule sets whenever possible. Don't bundle them all in a separate stylesheet or at the end of the document.
- Don't write vendor prefixes (configure autoprefixer instead).
- Avoid unnecessary nesting and too many nesting levels.
- Mixins and functions should be as simple as possible, serve only one purpose and be documented
- Use variables for colors, and units that are used in multiple locations
JavaScript is an open language, that let's you write code in very different ways, even in different paradigms, from prototypal object oriented to functional programming. In TAO we've selected some programming paradigms and patterns over others. The goal is to bring some consistency and shared practices across the platform.
"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system." Andy Hunt, The Pragmatic Programmer
The simple principle will lead to code easier to maintain. So if you write the same code multiple time, think about abstractions. Abstractions doesn't need to be too high level.
"Sometimes, the elegant implementation is just a function. Not a method. Not a class. Not a framework. Just a function." John Carmack, game developer
If your module needs to expose a function, then your module can expose only a function, especially when there's no state, no side effect!
If multiple functions serve the same purpose they can be grouped into an object serving multiple and independent static like methods :
//a case module util
const stringUtil = {
capitalize(inputString) {},
camelToSnake(inputString) {}
};
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand" Martin Fowler
When writing your module think about it as an API, following the open/close principle, think about input and output. Try to avoid side effect and try to think as the developer that will use this API : "how ideally would you like to call this API".
Using TDD can help in having clear APIs, testing first the API usually lead to clear APIs.
"You wanted a banana but what you got was a gorilla holding the banana and the entire jungle". Joe Armstrong, creator of Erlang, about the classical inheritance
To avoid strong coupling due to inheritance, we favor in TAO composition over classical inheritance. The main goal remains to separate the behavior from the implementation, in order to divide the responsibilities.
Composition can have multiple form, based on the use case :
- Aggregation
This simple pattern consists in using another module.
import jwtSignatureFactory from 'jwtSignatureFactory.js'
module.exports function jwtHandler(){
//we use another module in the current module
const jwtSignatureVerifier = jwtSignatureFactory('HMAC', 'SHA256');
}
- Mixin
This pattern consists in assigning the method of an object to another in order to aggregate them into one object, or give the feature of an object to another. For example,
//the mixin, a separate behavior you'll add on multiple objects
const assignee = {
getAssignments() {
return this.assignments;
},
setAssignments(deliveries) {
this.assignments = assignments;
}
};
const aUser = {
firstName: 'john',
lastName: 'snow',
getName() {
return `${this.firstName} ${this.lastName}`;
}
};
const testTaker = Object.assign(aUser, assignee);
The particularity of this pattern is the scope is shared between the target and the mixin : this
will be shared.
There are multiple ways to achieve this pattern, including using prototypes.
When to use it ? When mixins are pure methods or stateless by preference. There shouldn't be any strong coupling between the target and the mixin (for example the mixin expects a property to be available in the target).
- Delegation
Delegation pattern is composition pattern where component delegates functionality to other module
const person = {
name: 'Carl',
allowanceLimit: 20
};
const allowance = {
substract(amount) {
if (amount <= this.allowanceLimit) {
this.allowanceLimit -= amount;
}
},
getAllowanceLimit() {
return this.allowanceLimit;
}
};
function delegate(source, methods, provider) {
methods.forEach(methodName => {
source[methodName] = function (...args) {
return provider[methodName].apply(source, args);
};
});
}
delegate(person, ['substract', 'getAllowanceLimit'], allowance);
person.substract(5);
console.log(person.getAllowanceLimit()); //15
person.substract(10);
console.log(person.getAllowanceLimit()); //5
person.substract(10);
console.log(person.getAllowanceLimit()); //5 (did not substract because of limit)
delegate
function does late binding of provider
mathods to person objects.
Later we call delegate
to attach allowance operation from person
to allowance
module. Allowance methods then update person state
- Forwarding
Forwarding design pattern is used to completely forward data and control to other module. This is the good approach to manage shared states and distribute permission for shared state operation.
const person1 = {
name: 'Bob'
};
const person2 = {
name: 'Alice'
};
const familyWallet = {
balance: 0,
earn(amount) {
this.balance += amount;
},
spend(amount) {
this.balance -= amount;
},
getBalance() {
return this.balance;
}
};
function forward(person, methods, provider) {
methods.forEach(methodName => {
person[methodName] = function (...args) {
return provider[methodName](...args);
};
});
}
forward(person1, ['earn', 'spend', 'getBalance'], familyWallet);
forward(person2, ['spend', 'getBalance'], familyWallet);
person1.earn(30);
console.log(person1.getBalance()); // 30
person2.spend(10);
person1.spend(10);
console.log(person2.getBalance()); // 10
forward
function does late binding of provider
mathods to person objects.
Later we call forward
to attach wallet operation from person1
and person2
to familyWallet
.
- Adapter
It happens quite often when API of the module you want to delegate or forward the calls is a bit different. To keep consistency of modules and APIs you can use Adapter
pattern which has contains logic of API aligning.
Mind the previous example, but now we want to use secureFamilyWallet
which requires some key
to spend money
const secureFamilyWallet = {
// ...
spend(amount, key) {
this.balance -= amount;
}
// ...
};
function secureFamilyWalletAdapter(key, wallet) {
return {
//...
spend(amount) {
return secureFamilyWallet.spend.call(wallet, amount, key);
}
//...
};
}
You can now create the instance of secureFamilyWalletAdapter
with the key specified and forward person operations to it. No API change of person nor forward function is required.
"The best thing about JavaScript is its implementation of functions. It got almost everything right. But, as you should expect with JavaScript, it didn't get everything right." Douglas Crockford, JavaScript: The Good Parts
When a module needs to keep a state and hide some implementation details, the factory pattern will be selected.
const countDownFactory = function countDownFactory(config) {
let currentValue = config.value || 0; //private but accessible through the API
let interval = null; //kept private
return {
getValue: function getValue() {
//expose some internal va
return currentValue;
},
start: function start() {
interval = setInterval(function () {
currentValue--;
}, config.delay);
},
stop: function stop() {
clearInterval(interval);
},
reset: function reset() {}
};
};
"JavaScript is especially suited for event-driven programming, because of the callback pattern, which enables your programs to work asynchronously, in other words, out of order." Stoyan Stefanov, JavaScript Patterns
The goal of this pattern is to listen some events from a source and attach a behavior when they're triggered.
This is the pattern used by the DOM to react on user's interactions, like a click
(see DOM Events. The node.js EventEmitter is also a popular implementation of this pattern.
For example ,
const countdown = eventifier({
start(counter){
if(!this.started){
this.value = counter;
this.interval = setInterval( () => {
this.value--;
this.trigger('update', value);
if(this.value <= 0){
this.stop();
}
});
this.trigger('start');
this.started = true;
}
},
stop(){
if(this.started){
clearInterval(this.interval);
this.trigger('stop');
this.started = false
}
}
});
countdown
.on('update', value => console.log(`Please wait ${value}seconds.`))
.on('stop', () => console.log('Please enter'))
.start();
TAO provides an implementation, the core/eventifier
module, it has the following features :
- contextualized to an object (events are scoped)
- support AOP style listening (
before
->on
->after
) - support namespaces
- support
Promise
(asynchronous handlers) - supports context spreading
Please check out the eventifier documentation.
When multiples implementation of a given API can be defined, or dynamically defined, the provider pattern is used.
const jsonDataProvider = {
requestData() {
//read some json file and return data
}
};
const csvDataProvider = {
requestData() {
//read some csv file and return data
}
};
const person = {
dataProvider,
data,
updateData() {
this.data = this.dataProvider.requestData();
},
registerProvider(provider) {
this.dataProvider = provider;
}
};
In this example two providers are defined, which have same requestData
API method. person
object does not request data directly and doesn't implement any data parsing logic, it just registers the appropriated provider and request data from it. This allows to decouple components, keep components small and easy testable.
You can combine the provider with forwarding/delegation patterns and delegate some common routines to shared modules (i.e. authentication or validation).
In TAO we render and manipulate DOM using "components". A component is a chunk of the graphical user interface. A component can be atomic (a button) or a larger part of the GUI (a dashboard) that uses other components. The atomicity level is up to use cases and usually defined by the way the component will be used. The purpose of a component is to render a DOM from a given set of data, react to the time and user events, and change based on a given state.
An article is dedicated to components.
The way to do components in TAO has evolved a lot and only stabilized a few years ago, but expect the way to build component to be changed again soon. Remember if the way change the concept remains the same.
An article is dedicated to plugins.