Opinionated RiotJS Style Guide for teams by De Voorhoede.
This guide provides a uniform way to structure your RiotJS code. Making it
This guide is inspired by the AngularJS Style Guide by John Papa.
Our RiotJS demos are a companion to this guide, illustrating the guidelines with practical examples.
*.tag.html
extension<script>
inside tagthis
to tag
tag.parent
each ... in
syntaxAlways construct your app out of small modules which do one thing and do it well.
A module is a small self-contained part of an application. The RiotJS micro-framework is specifically designed to help you create view-logic modules, which Riot calls tags.
Small modules are easier to learn, understand, maintain, reuse and debug. Both by you and other developers.
Each riot tag (like any module) must be FIRST: Focused (single responsibility), Independent, Reusable, Small and Testable.
If your module does too much or gets too big, split it up into smaller modules which each do just one thing. As a rule of thumb, try to keep each tag file less than 100 lines of code. Also ensure your tag module works in isolation. For instance by adding a stand-alone demo.
Tip: If you use an AMD or CommonJS module loader, you need to compile your tags using the --modular
(-m
) flag:
# enable AMD and CommonJS
riot --modular
A tag module is a specific type of module, containing a Riot tag.
Each module name must be:
Tag module names must also be:
app-
namespaced: if very generic and otherwise 1 word, so that it can easily be reused in other projects.tag
element is inserted into the DOM. As such, they need to adhere to the spec.<!-- recommended -->
<app-header />
<user-list />
<range-slider />
<!-- avoid -->
<btn-group /> <!-- short, but unpronouncable. use `button-group` instead -->
<ui-slider /> <!-- all tags are ui elements, so is meaningless -->
<slider /> <!-- not custom element spec compliant -->
Bundle all files which construct a module into a single place.
Bundling module files (Riot tags, tests, assets, docs, etc.) makes them easy to find, move and reuse.
Use the module name as directory name and file basename. The file extension depends on the purpose of the file.
modules/
└── my-example/
├── my-example.tag.html
├── my-example.less
├── ...
└── README.md
If your project uses nested structures, you can nest a module within a module.
For example a generic radio-group
module may be placed directly inside "modules/". While search-filters
may only make sense inside a search-form
and may therefore be nested:
modules/
├── radio-group/
| └── radio-group.tag.html
└── search-form/
├── search-form.tag.html
├── ...
└── search-filters/
└── search-filters.tag.html
*.tag.html
extensionRiot introduces a new concept called tags, and suggests to use a *.tag
extension.
However in essence these tags are simply custom elements containing markup. Therefore you should *use the `.tag.html` extension**.
In case of in-browser compilation:
<script src="https://github.com/voorhoede/riotjs-style-guide/raw/master/path/to/modules/my-example/my-example.tag.html" type="riot/tag"></script>
In case of pre-compilation, set the custom extension:
riot --ext tag.html modules/ dist/tags.js
In case you're using the Webpack tag loader, configure the loader to match the extension:
{ test: /\.tag.html$/, loader: 'tag' }
<script>
inside tagWhile Riot supports writing JavaScript inside a tag element without a <script>
,
you should always use <script>
around scripting. This is closer to web standards and prevents confusing developers and IDEs.
<!-- recommended -->
<my-example>
<h1>The year is { this.year }</h1>
<script>
this.year = (new Date()).getUTCFullYear();
</script>
</my-example>
<!-- avoid -->
<my-example>
<h1>The year is { this.year }</h1>
this.year = (new Date()).getUTCFullYear();
</my-example>
Riot's inline expressions are 100% Javascript. This makes them extemely powerful, but potentially also very complex. Therefore you should keep tag expressions simple.
Move complex expressions to tag methods or tag properties.
<!-- recommended -->
<my-example>
{ year() + '-' + month() }
<script>
const twoDigits = (num) => ('0' + num).slice(-2);
this.month = () => twoDigits((new Date()).getUTCMonth() +1);
this.year = () => (new Date()).getUTCFullYear();
</script>
</my-example>
<!-- avoid -->
<my-example>
{ (new Date()).getUTCFullYear() + '-' + ('0' + ((new Date()).getUTCMonth()+1)).slice(-2) }
</my-example>
Riot supports passing options to tag instances using attributes on tag elements. Inside the tag instance these options are available through opts
. For example the value of my-attr
on <my-tag my-attr="{ value }" />
will be available inside my-tag
via opts.myAttr
.
While Riot supports passing complex JavaScript objects via these attributes, you should try to keep the tag options as primitive as possible. Try to only use JavaScript primitives (strings, numbers, booleans) and functions. Avoid complex objects.
Exceptions to this rule are situations which can only be solved using objects (eg. collections or recursive tags) or well-known objects inside your app (eg. a product in a web shop).
Use a tag attribute per option, with a primitive or function as value:
<!-- recommended -->
<range-slider
values="[10, 20]"
min="0"
max="100"
step="5"
on-slide="{ updateInputs }"
on-end="{ updateResults }"
/>
<!-- avoid -->
<range-slider config="{ complexConfigObject }">
<!-- exception: recursive tag, like menu item -->
<menu-item>
<a href="https://github.com/voorhoede/riotjs-style-guide/blob/master/{ opts.url }">{ opts.text }</a>
<ul if="{ opts.items }">
<li each="{ item in opts.items }">
<menu-item
text="{ item.text }"
url="{ item.url }"
items="{ item.items }" />
</li>
</ul>
</menu-item>
In Riot your tag options are your API. A robust and predictable API makes your tags easy to use by other developers.
Tag options are passed via custom HTML attributes. The values of these attributes can be Riot expressions (attr="{ var }"
) or plain strings (attr="value"
) or missing entirely. You should harness your tag options to allow for these different cases.
Harnessing your tag options ensures your tag will always function (defensive programming). Even when other developers later use your tags in ways you haven't thought of yet.
For instance Riot's <todo>
example could be improved to also work if no items
are provided, by using a default:
this.items = opts.items || []; // default to empty list
Ensuring different use cases all work:
<todo items="{ [{ title:'Apples' }, { title:'Oranges', done:true }] }"> <!-- uses given list -->
<todo> <!-- uses empty list -->
The <range-slider>
in keep tag options primitive expects numbers for min
, max
and step
. Use type conversion:
// if step option is valid, use as number otherwise default to one.
this.step = !isNaN(Number(opts.step)) ? Number(opts.step) : 1;
Ensuring different use cases all work:
<range-slider> <!-- uses default -->
<range-slider step="5"> <!-- converts "5" to number 5 -->
<range-slider step="{ x }"> <!-- tries to use `x` -->
The <range-slider>
also supports optional on-slide
and on-end
callback. Check option exists and is in expected format before using it:
slider.on('slide', (values, handle) => {
if (typeof opts.onSlide === 'function') {
opts.onSlide(values, handle);
}
}
Ensuring different use cases all work:
<range-slider> <!-- does nothing -->
<range-slider on-slide="{ updateInputs }"> <!-- calls updateInputs on slide -->
<range-slider on-slide="invalid option"> <!-- does nothing -->
this
to tag
Within the context of a Riot tag element, this
is bound to the tag instance.
Therefore when you need to reference it in a different context, ensure this
is available as tag
.
this
to a variable named tag
the variable tells developers it's bound to the tag instance wherever it's used./* recommended */
// ES5: assign `this` to `tag` variable
var tag = this;
window.onresize = function() {
tag.adjust();
}
// ES6: assign `this` to `tag` constant
const tag = this;
window.onresize = function() {
tag.adjust();
}
// ES6: you can still use `this` with fat arrows
window.onresize = () => {
this.adjust();
}
/* avoid */
var self = this;
var _this = this;
// etc
Inside a Riot tag element you typically put its markup first, followed by its script.
Properties and methods bound to the tag (this
) in the script are directly available in the markup. You should put those tag properties and methods alphabetized at the top of the script. Tag methods longer than a one-liner should be linked to separate functions later in the script.
Put tag properties and methods on top:
/* recommended: alphabetized properties then methods */
var tag = this;
tag.text = '';
tag.todos = [];
tag.add = add;
tag.edit = edit;
tag.toggle = toggle;
function add(event) {
/* ... */
}
function edit(event) {
/* ... */
}
function toggle(event) {
/* ... */
}
/* avoid: don't spread out tag properties and methods over script */
var tag = this;
tag.todos = [];
tag.add = function(event) {
/* ... */
}
tag.text = '';
tag.edit = function(event) {
/* ... */
}
tag.toggle = function(event) {
/* ... */
}
Also put mixins and observables up top:
/* recommended */
var tag = this;
// alphabetized properties
// alphabetized methods
tag.mixin('someBehaviour');
tag.on('mount', onMount);
tag.on('update', onUpdate);
// etc
Riot supports a shorthand ES6 like method syntax. Riot compiles the shorthand syntax methodName() { }
into this.methodName = function() {}.bind(this)
. Since this is non-standard you should avoid fake ES6 method shorthand syntax.
Use tag.methodName =
instead of magic methodName() { }
syntax:
/* recommended */
var tag = this;
tag.todos = [];
tag.add = add;
function add() {
if (tag.text) {
tag.todos.push({ title: tag.text });
tag.text = tag.input.value = '';
}
}
/* avoid */
todos = [];
add() {
if (this.text) {
this.todos.push({ title: this.text });
this.text = this.input.value = '';
}
}
Tip: Disable transformation of the fake ES6 syntax during pre-compilation by setting type
to none
:
riot --type none
tag.parent
Riot supports nested tags which have access to their parent context through tag.parent
. Accessing context outside your tag module violates the FIRST rule of module based development. Therefore you should avoid using tag.parent
.
The exception to this rule are anonymous child tags in a for each loop as they are defined directly inside the tag module.
<!-- recommended -->
<parent-tag>
<child-tag value="{ value }" /> <!-- pass parent value to child -->
</parent-tag>
<child-tag>
<span>{ opts.value }</span> <!-- use value passed by parent -->
</child-tag>
<!-- avoid -->
<parent-tag>
<child-tag />
</parent-tag>
<child-tag>
<span>value: { parent.value }</span> <!-- don't do this -->
</child-tag>
<!-- recommended -->
<parent-tag>
<child-tag on-event="{ methodToCallOnEvent }" /> <!-- use method as callback -->
<script>this.methodToCallOnEvent = () => { /*...*/ };</script>
<parent-tag>
<child-tag>
<button onclick="{ opts.onEvent }"></button> <!-- call method passed by parent -->
</child-tag>
<!-- avoid -->
<parent-tag>
<child-tag />
<script>this.methodToCallOnEvent = () => { /*...*/ };</script>
<parent-tag>
<child-tag>
<button onclick="{ parent.methodToCallOnEvent }"></button> <!-- don't do this -->
</child-tag>
<!-- allowed exception -->
<parent-tag>
<button each="{ item in items }"
onclick="{ parent.onEvent }"> <!-- okay, because button is not a riot tag -->
{ item.text }
</button>
<script>this.onEvent = (e) => { alert(e.item.text); }</script>
</parent-tag>
each ... in
syntaxRiot supports multiple notations for loops: item in array (each="{ item in items }"
); key, value in object (each="{ key, value in items }"
) and a shorthand (each="{ items }"
) notation. This shorthand can lead to confusion. Therefore you should use the each ... in
syntax.
Riot creates a new tag instance for each item the each
directive loops through. When using the shorthand notation, the methods and properties of the current item are bound to the current tag instance (local this
). This is not obvious when looking at the markup and may thus confuse other developers. Therefore you should use the each ... in
syntax.
Use each="{ item in items }"
or each="{ key, value in items }"
instead of each="{ items }"
syntax:
<!-- recommended: -->
<ul>
<li each="{ item in items }">
<label class="{ completed: item.done }">
<input type="checkbox" checked="{ item.done }"> { item.title }
</label>
</li>
</ul>
<!-- recommended: -->
<ul>
<li each="{ key, item in items }">
<label class="{ completed: item.done }">
<input type="checkbox" checked="{ item.done }"> { key }. { item.title }
</label>
</li>
</ul>
<!-- avoid: -->
<ul>
<li each="{ items }">
<label class="{ completed: done }">
<input type="checkbox" checked="{ done }"> { title }
</label>
</li>
</ul>
For developer convenience, Riot allows you to define a tag element's style in a nested <style>
tag. While you can scope these styles to the tag element, Riot does not provide true encapsulation. Instead Riot extracts these styles from the tags (JavaScript) and injects them into the document on runtime. Since Riot compiles nested styles to JavaScript and doesn't have true encapsulation, you should instead put styles in external files.
<style>
s so there's no added benefit in using them.Styles related to the tag and its markup, should be placed in a separate stylesheet file next to the tag file, inside its module directory:
my-example/
├── my-example.tag.html
├── my-example.(css|less|scss) <-- external stylesheet next to tag file
└── ...
Riot tag elements are custom elements which can very well be used as style scope root. Alternatively the module name can be used as CSS class namespace.
Use the tag name as selector, as parent selector or as namespace prefix (depending on your CSS naming strategy).
/* recommended */
my-example { }
my-example li { }
.my-example__item { }
/* avoid */
.my-alternative { } /* not scoped to tag or module name */
.my-parent .my-example { } /* .my-parent is outside scope, so should not be used in this file */
note: If you're using data-is=
(introduced in v2.3.17) to initiate Riot tags, you can use [data-is="my-example"]
as CSS selector instead of .my-example
.
A Riot tag instance is created by using the tag element inside your application. The instance is configured through its custom attributes. For the tag to be used by other developers, these custom attributes - the tag's API - should be documented in a README.md
file.
README.md
is the de facto standard filename for documentation to be read first. Code repository hosting services (Github, Bitbucket, Gitlab etc) display the contents of the the README's, directly when browsing through source directories. This applies to our module directories as well.Add a README.md
file to the tag's module directory:
range-slider/
├── range-slider.tag.html
├── range-slider.less
└── README.md
Within the README file, describe the functionality and the usage of the module. For a tag module its most useful to describe the custom attributes it supports as those are its API:
# Range slider
## Functionality
The range slider lets the user to set a numeric range by dragging a handle on a slider rail for both the start and end value.
This module uses the [noUiSlider](http://refreshless.com/nouislider/) for cross browser and touch support.
## Usage
`<range-slider>` supports the following custom tag attributes:
| attribute | type | description
| --- | --- | ---
| `min` | Number | number where range starts (lower limit).
| `max` | Number | Number where range ends (upper limit).
| `values` | Number[] *optional* | Array containing start and end value. E.g. `values="[10, 20]"`. Defaults to `[opts.min, opts.max]`.
| `step` | Number *optional* | Number to increment / decrement values by. Defaults to 1.
| `on-slide` | Function *optional* | Function called with `(values, HANDLE)` while a user drags the start (`HANDLE == 0`) or end (`HANDLE == 1`) handle. E.g. `on-slide={ updateInputs }`, with `tag.updateInputs = (values, HANDLE) => { const value = values[HANDLE]; }`.
| `on-end` | Function *optional* | Function called with `(values, HANDLE)` when user stops dragging a handle.
For customising the slider appearance see the [Styling section in the noUiSlider docs](http://refreshless.com/nouislider/more/#section-styling).
Add a *.demo.html
file with demos of the tag with different configurations, showing how the tag can be used.
Add a *.demo.html
file to your module directory:
city-map/
├── city-map.tag.html
├── city-map.demo.html
├── city-map.css
└── ...
Inside the demo file:
riot+compiler.min.js
to also compile during runtime../city-map.tag.html
).demo
tag (<yield/>
) to embed your demos in (otherwise option attributes are not supported).<demo>
tags.aria-label
s to the <demo>
tags and style those as title bars.riot.mount('demo', {})
.Example demo file in city-tag
module:
<!-- modules/city-map/city-map.demo.html: -->
<body>
<h1>city-map demos</h1>
<demo aria-label="City map of London">
<city-map location="London" />
</demo>
<demo aria-label="City map of Paris">
<city-map location="Paris" />
</demo>
<link rel="stylesheet" href="https://github.com/voorhoede/riotjs-style-guide/blob/master/city-map.css">
<script src="https://github.com/voorhoede/riotjs-style-guide/raw/master/path/to/riot+compiler.min.js"></script>
<script type="riot/tag" src="https://github.com/voorhoede/riotjs-style-guide/raw/master/city-map.tag.html"></script>
<script>
riot.tag('demo','<yield/>');
riot.mount('demo', {});
</script>
<style>
/* add a grey bar with the `aria-label` as demo title */
demo:before {
content: "Demo: " attr(aria-label);
display: block;
background: #F3F5F5;
padding: .5em;
clear: both;
}
</style>
</body>
Note: this is a working concept, but could be much cleaner using build scripts.
Linters improve code consistency and help trace syntax errors. With some extra configuration Riot tag files can also be linted.
To allow linters to extract the scripts from your *.tag.html
files, put script inside a <script>
tag and keep tag expressions simple (as linters don't understand those). Configure your linter to allow global variables riot
and tag opts
.
ESLint requires an extra ESLint HTML plugin to extract the script from the tag files.
Configure ESLint in modules/.eslintrc
(so IDEs can interpret it as well):
{
"extends": "eslint:recommended",
"plugins": ["html"],
"env": {
"browser": true
},
"globals": {
"opts": true,
"riot": true
}
}
Run ESLint
eslint modules/**/*.tag.html
JSHint can parse HTML (using --extra-ext
) and extract script (using --extract=auto
).
Configure JSHint in modules/.jshintrc
(so IDEs can interpret it as well):
{
"browser": true,
"predef": ["opts", "riot"]
}
Run JSHint
jshint --config modules/.jshintrc --extra-ext=html --extract=auto modules/
Note: JSHint does not accept tag.html
as extension, but only html
.
You can use the RiotJS Style Guide badge to link to this guide:
Inform other developers your project is following the RiotJS Style Guide. And let them know where they can find this guide.
Include the badge in your project. In markdown:
[![RiotJS Style Guide badge](https://cdn.rawgit.com/voorhoede/riotjs-style-guide/master/riotjs-style-guide.svg)](https://github.com/voorhoede/riotjs-style-guide)
Or html:
<a href="https://github.com/voorhoede/riotjs-style-guide">
<img alt="RiotJS Style Guide badge"
src="https://cdn.rawgit.com/voorhoede/riotjs-style-guide/master/riotjs-style-guide.svg">
</a>
De Voorhoede waives all rights to this work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.