API Documentation annotated source

Back to index

        

This is an example-driven documentation for Torus's entire API surface. If you want to know more about Torus as an open-source project, or what it is, check out the Github repository.

4

Using Torus with your project

6

If you're in the browser, you can import Torus with a script tag.

8<script src="https://unpkg.com/torus-dom/dist/index.min.js"></script>
9

Imported like this, Torus exposes its entire API through these globals.

11window.Torus; // contains all of the core library, including `Torus.Component`, etc.
12window.jdom; // contains the `jdom` template tag
13window.css; // contains the `css` template tag
14

.. so you can create a component, for example, like this. We'll omit the Torus. prefix for the rest of this documentation, for sake of brevity.

18class MyComponent extends Torus.Component {
19    /* ... */
20}
21

If you're bundling within NodeJS, the torus-dom package contains all the exported names. You can use ES2015 imports...

24import { Component, Record, Store, jdom, css } from 'torus-dom';

... or CommonJS require.

26const { Component, Record, Store, jdom, css } = require('torus-dom');
27

JDOM

29

Torus, like React, Preact, and Vue, uses a virtual DOM to reconcile differences between a previous render of a component and the new render of a component before committing any changes to the DOM. To run the reconciliation algorithm, we need an intermediate representation of the component render tree that's efficient to create and traverse. Torus achieves this with JDOM, a JSON format for representing all DOM nodes.

36

null values are always translated to comments

38null; // <!---->
39

String values represent text (they map to TextNodes)

41'Hello, World!'; // 'Hello, World!'
42

HTML elements are represented with an object with four keys: tag, attrs, events, and children. tag is the name of the HTML tag for the element. All fields are optional, except tag, which is required.

46const el = {
47    tag: 'ul',
48    attrs: {},
49    events: {},
50    children: [],
51} // <ul></ul>
52

attrs is an object containing all attributes and properties of the element. attrs.class is a special property, because it can either be a string representation of the element's className property, or an array of class names. If it's an array, the members of that array will be joined together during render. Other attrs properties should be strings or numbers.

58const $input = {
59    tag: 'input',
60    attrs: {
61        class: ['formInput', 'lightblue'],
62        'data-importance': 5,
63        value: '42.420',
64    },
65} //<input class="formInput lightblue"
66  //       data-importance="5" value="42.420" />
67

Although Torus advocates for using the Styled() higher-order component when styling components, sometimes it's useful to have access to inline styles. Inline styles are represented in JDOM as a flat object, using CSS-style camelCase CSS property names.

71const $fancyButton = {
72    tag: 'button',
73    attrs: {
74        style: {
75            color: 'red',
76            backgroundColor: 'blue',
77            fontSize: '16px',
78        },
79    },
80} // <button style="color:red;background-color:blue;font-size: 16px"></button>
81

events is a dictionary of events and event listeners. Each key in the dictionary is an event name, like 'click' or 'mouseover', and each value is either a single function or an array of functions that are handlers for the event. Torus does not currently delegate events. Each event is bound to the given element directly. However, this may change in the future as Torus is used in more complex UIs.

87const $button = {
88    tag: 'button',
89    events: {
90        click: () => alert('Button clicked!'),
91        mouseover: [
92            evt => console.log(evt.clientX),
93            evt => console.log(evt.clientY),
94        ],
95    }
96}
97

children is a flat array of the element's children, in JDOM format. This means children can be any string, nulls, or other JDOM "nodes" as objects.

100const $container = {
101    // Implied 'div'
102    children: [
103        'πŸ‘‹',
104        'Hello, ',
105        {
106            tag: 'em',
107            children: ['World!'],
108        },
109    ],
110} //> <div>πŸ‘‹Hello, <em>World!</em></div>
111

The jdom template tag

113

These basic rules allow us to compose relatively complex elements that are still readable, but JDOM is an intermediate representation used for reconciliation and rendering, and isn't suitable for writing good code. To help writing component markup easier, Torus comes with a helper function in the form of a template tag.

119
120jdom`<p>Hi</p>`; // creates JDOM for <p>Hi</p>
121

This function, called jdom, allows us to write in plain HTML markup with variables and dynamic template values mixed in, and it'll translate our markup to valid JDOM during runtime. This means there's no compilation and no build step. Despite the fact that templates are parsed at runtime, because each template is cached on the first read, rendering remains fast.

127
128let name = 'Dan';

We can embed variables into jdom templates.

130jdom`<strong>
131    My name is ${name}
132</strong>`;
133

Using the jdom template tag, we can express everything that JDOM can represent using HTML-like markup. Here, we bind an event listener to this <input> element by passing in a function as an onchange value.

137jdom`<input
138    type="text"
139    value="${user_input_value}"
140    onchange="${evt => console.log(evt.target.value)}"
141/>`;
142

Of course, we can embed children elements inside jdom templates. The template tag allows children to be either a single element or JDOM object, or an array of element (HTML node) or JDOM objects. This allows nesting sub-lists in lists easy.

147const snippet = jdom`<h1>This is a title</h1>`;
148const listPart = [
149    document.createElement('li'),
150    document.createElement('li'),
151    document.createElement('li'),
152];
153jdom`<div class="container">
154    ${snippet}
155    <ul>${listPart}</ul>
156</div>`;
157

As a bonus, quotes are optional...

159jdom`<input type=text value=default-value />`;
160

... and general closing tags </> can close out their corresponding tags, although this can make for less readable code and isn't recommended most of the time. This can come in handy if our tag name changes conditionally.

164const tagName = isEven(someNumber) ? 'em' : 'strong';
165jdom`<span><${tagName}>${someNumber} is my favorite number.</></span>`;
166

An important note on security

168

Inline <script> tags are well known to be a source of many tricky security issues, and are generally best avoided unless it's the only way to solve your problem at hand. Inline scripts are especially problematic in templating code like jdom because it's easy to write templates and accidentally forget to escape user input when rendering it into DOM.

173

To help alleviate the potential security risks here, no user-provided input (no variable passed into jdom templates inside curly braces) will ever be parsed into HTML by the template processor. This prevents potential cross-site scripting issues or premature <script> tag terminations, for example, but it means that you'll have to wrap any template string in jdom tags, even if it's inside another jdom tagged template.

179

On a related note, if you must use an inline <script> in a template, jdom allows this (for now). However, you'll want to escape the entire contents of the script tag, by wrapping the full contents of the tag in curly braces, to avoid security pitfalls around escaping code.

183jdom`<script>${'console.log("This is highly discouraged, for security reasons.")'}</script>`;
184

Component

186

In Torus, we build user interfaces by composing and connecting reusable components together. A Torus component extends the base Component class, and has a #compose() method that returns the JDOM representation of the UI component's DOM. Torus uses this method to render the component. Because Torus renders components declaratively, we should avoid calling methods on the component (like mutating the local state) within #compose(). Mutating state during render may lead to race conditions or infinite loops, since state mutations are usually linked to more render calls.

194class MyFirstComponent extends Component {
195
196    compose() {
197        return {
198            tag: 'h1',
199            children: ['Hello, World!'],
200        }
201    }
202
203}
204

... of course, we can simplify this by relying on the jdom template tag

206class MySecondComponent extends Component {
207
208    compose() {
209        return jdom`<h1>Hello, World!</h1>`;
210    }
211
212}
213

Now that we've defined a component, we can render that component to our page. Each component has a #node property, which always represents the root DOM node of that component. To add our component to the page, we can just create a new instance of our component and add its node to the document.

218const first = new MyFirstComponent();

We use the native appendChild() API to add our node to the page.

220document.body.appendChild(first.node);
221

All components have a #render() method that's provided by Torus. The render method is owned by Torus, and we rarely need to override it, because we tell torus what to render with #compose(). But if we want to tell torus to re-compose and re-render our component, we can do so by calling Component#render().

226const second = new MySecondComponent(); // rendered once, by default, on initialization
227second.render(); // rendered a second time
228

Torus intelligently only mutates the DOM on the page if anything has changed, so renders are quick and efficient (but not free -- we should still only call render when we have something new to display).

232

Sometimes, we need to create components with different properties each time. We define the initial state of our component in an #init() method, which is called with the arguments passed in when we construct an instance of our component. The init method is also a good place to bind methods and listen to event targets.

237class Tweet extends Component {
238

This is called from the parent class's constructor. Why is it called init() and not constructor(), you might ask? There are a few reasons why I made this call. Primarily, this ensured that components always had a valid and correct #node property by allowing Torus to render all components in the constructor. This allows for the component system to have a generally more ergonomic API. We can read and override Component#super() if we want to extend Torus's functionality.

246    init(author, content) {
247        this.author = author;
248        this.content = content;

For some reason, if we want this component to update every time the window is resized, we can do add kinds of listeners here.

251        this.render = this.render.bind(this);
252        window.addEventListener('resize', this.render);
253    }
254

Since we bound a listener in init(), we need to make sure we clean up after ourselves.

257    remove() {
258        window.removeEventListener('resize', this.render);
259    }
260

We can access the instance and class variables within our JDOM template, because it's just a JavaScript method.

263    compose() {
264        return jdom`<div class="tweet">
265            <div>${this.content}</div>
266            <cite>${this.author}</cite>
267            <p>The window size is now ${window.innerWidth}</p>
268        </div>`;
269    }
270
271}
272

We can have components with event listeners and other methods that we use to respond to events

275class FancyInput extends Component {
276
277    init() {
278        this.onFocus = this.onFocus.bind(this);
279        this.onBlur = this.onBlur.bind(this);
280    }
281
282    onFocus() {
283        console.log('Focused input');
284    }
285
286    onBlur() {
287        console.log('Blurred input');
288    }
289
290    compose() {
291        return jdom`<input type=text
292            onfocus=${this.onFocus}
293            onblur=${this.onBlur} />`;
294    }
295
296}
297

If we need to fetch data within a component, here's one way to do so, by re-rendering when new data arrives. Let's say we want a component that displays a list of the top 10 winners in some competition, using a JSON API.

301class RankingsList extends Component {
302

Let's say we want this component to display top 10 places by default

304    init(limit = 10) {
305        this.limit = limit;
306        this.rankings = [];

We don't always want to fetch when we create a view, but just for today, let's say we'll fetch for new data when this view first appears.

309        this.fetch();
310    }
311
312    async fetch() {
313        const data = fetch('/api/v2/rankings?limit=10').then(r = r.json());

Only take the first this.limit results

315        this.rankings = data.slice(0, this.limit);

Since data's changed, we'll re-render the view

317        this.render();
318    }
319
320    compose() {
321        return jdom`<ol>
322            ${this.rankings.map(winner => {

We can nest a template within a template to render a very simple list like this. For more complex lists, we'll want to use a List component, like the one below, where each item gets its own component.

327                return jdom`<li>
328                    ${winner.name}, from ${winner.city}
329                </li>`;
330            })}
331        </ol>`;
332    }
333
334}
335

One notable difference between Torus's and React's component API, which this somewhat resembles, is that Torus components are much more self-managing. Torus components are long-lived and have state, and so have no lifecycle methods, but instead call #render imperatively whenever the component's DOM tree needs to update (when local state changes or a data source emits an event). This manual render-call replaces React's shouldComponentUpdate in a sense, and means that render functions are never run unless a re-render is explicitly requested as the result of a state change, even if the component in question is a child of a parent that re-renders frequently.

344class AppWithSidebar extends Component {
345
346    init() {

As a downside of that tradeoff, we need to keep track of children components ourselves...

349        this.sidebar = new Sidebar();
350        this.main = new MainPanel();
351    }
352
353    remove() {

... but most of that just means instantiating and removing children views inside parent views, like this.

356        this.sidebar.remove();
357        this.main.remove();
358    }
359
360    compose() {

Because we can embed HTML nodes inside jdom templates, we can include our children components in the parent component's DOM tree easily, like this.

363        return jdom`<div class="root">
364            ${this.sidebar.node}
365            ${this.main.node}
366        </div>`;
367    }
368
369}
370

Component.from() and functional components

372

Often, we want to create a new Component class whose rendered result only depends on its initial input. These might be small UI widgets in a design system like buttons and dialogs, for example. In these cases where we don't need a component to keep local state, listen for events, or create side effects, we can describe the component's view with a pure function.

377

Here's an example of a purely functional component. It returns a JDOM representation. To distinguish functional components from class-based Torus components in usage, by convention, we name functional components with a name that begins with a lowercase letter. This distinction is helpful, because we use the new keyword with class components, and call their compose() method to get JDOM, while we call functional components directly to get JDOM.

383const fancyButton = buttonText =>
384    jdom`<button class="fancy">${buttonText}</button>`;
385

Because these kinds of function "components" will return valid JDOM, we can compose them into any other Component#compose() method. Remember, fancyButton is just a JavaScript function! This allows for easy abstraction and code reuse within small parts of our rendering logic. Here, we can just pass the return value of fancyButton() off to another template.

390class FancyForm extends Component {
391    compose() {
392        return `<form>
393            <h1>Super fancy form!</h1>
394            <input name="full_name"/>
395            ${fancyButton('Submit!')}
396        </form>`;
397    }
398}
399

But what if we want to compose our fancyButton with Styled() or use it in more complex views? We can upgrade the function component fancyButton to a full-fledged Torus class component with Component.from(). These two ways of defining fancyButton are equivalent. Here, ClassyButton emits exactly the same DOM as fancyButton when rendered, but it's gained additional capabilities accessible to a class component in Torus, like persistence across renders of the parent component and better composability with Styled() and ListOf().

406const ClassyButton = Component.from(fancyButton);

This takes a purely functional component and returns a class component whose constructor takes the same arguments (buttonText in this case) and renders to the given JDOM.

409const ClassyButton = Component.from(buttonText => {
410    return jdom`<button class="fancy">${buttonText}</button>`;
411});
412

We can use these upgraded components this way, by creating an instance of it and accessing its #node.

415class FancyForm extends Component {
416    init() {
417        this.button = new ClassyButton('Submit!');
418    }
419    compose() {
420        return `<form>
421            <h1>Super fancy form!</h1>
422            <input name="full_name"/>
423            ${this.button.node}
424        </form>`;
425    }
426}
427

Advanced topics in components

429

Component lifecycles

431

Because Torus components are designed to be long-lived, it's useful to think about the "life cycle" of a component, and what methods are called on it by Torus from its creation to its destruction. Currently, it's a short list.

435const comp = new Component();
436// 1. Create new component
437comp.init()
438// 2. Render triggered (can happen through `#bind(...)` binding the component
439//      to state updates, or through manual triggers from local state changes)
440comp.render();
441    comp.compose(); // called by render()
442// 3. Remove component (cleanly destroy component to be garbage collected)
443comp.remove();
444    comp.unbind(); // called by remove()
445

Note: It's generally a good idea to call Component#remove() when the component no longer becomes needed, like when a model tied to it is destroyed or the user switches to a completely different view. But because Torus components are lightweight, there's no need to use it as a memory management mechanism or to trigger garbage collection. Torus components, unlike React components, are designed to be long-lived and last through a full session or page lifecycle.

451

Accessing stateful HTMLElement methods

453

Torus tries to define UI components as declaratively as possible, but the rest of the web, and crucially the DOM, expose stateful APIS, like VideoElement.play() and FormElement.submit(). React's approach to resolving this conflict is refs, which are declaratively defined bindings to HTML elements that are later created by React's renderer. Torus's Component API is lower-level, and tries to expose the underlying DOM more transparently. Because we can embed literal DOM nodes within what Component#compose() returns, we can do this instead:

460class MyVideoPlayer extends Component {
461

Option A

463    init() {

Create a new element when the component is created, and keep track of it.

466        this.videoEl = document.createElement('video');
467        this.videoEl.src = '/videos/abc';
468    }
469
470    play() {
471        this.videoEl.play();
472    }
473
474    compose() {

We can compose a literal element within the root element

476        return jdom`<div id="player">
477            ${this.videoEl}
478        </div>`;
479    }
480
481}
482
483class MyForm extends Component {
484

Option B

486    submit() {

Because the root element of this component is the <form/>, this.node corresponds to the form element.

489        this.node.submit();
490    }
491
492    getFirstInput() {

This isn't ideal. I don't think this will be a commonly required pattern.

495        return this.node.querySelector('input');
496    }
497
498    compose() {
499        return jdom`<form>
500            <input type="text"/>
501        </form>`;
502    }
503
504}
505

Torus might get a higher-level API later to do something like this, maybe closer to React refs, but this is the current API design while larger Torus-based applications are built and patterns are better established.

507

Styled components and Component#preprocess()

509

Although you can use Torus perfectly without using CSS in JS, if you like declaring your view styles in your component code, as I do, Torus has an efficient and well-integrated solution, called styled components (not to be confused with React's styled-components, which has a similar idea but wildly different API).

515

You can create a styled Torus component by calling Styled() on a normal component

517class NormalButton extends Component { /*...*/ }
518const StyledButton = Styled(NormalButton);
519

But Torus already comes with StyledComponent, which is just Styled(Component). You should extend StyledComponent for new components, and only use the Styled() wrapper if you don't have access to the original component's code, such as when styling the default List implementation (below).

524const StyledComponent = Styled(Component); // literally what's in Torus's source code
525

Here's a sample of the ways you can define styles this way.

527class FancyList extends StyledComponent {
528

We define all of our styles in a styles() method, which returns a JSON that resembles normal CSS, but with nesting and automagical media query resolution, as well as downward scoping of CSS rules to this component. That means that when we style button in this component, it won't ever conflict with other button elements anywhere else on the page.

534    styles() {
535        return {

Normal CSS properties are applied to the root node

537            'font-size': '18px',
538            'background': 'rgba(0, 0, 0, .4)',
539

Keyframes and media queries will be scoped globally, just like in CSS

542            '@keyframes some-name': {
543                'from': {'opacity': 0},
544                'to': {'opacity': 1},
545            },
546

Note that we don't select the element again inside @media -- that's done for us by styled components.

549            '@media (min-width: 600px)': {
550                'border-color': 'rgb(0, 1, 2)',
551                'p': {
552                    'font-style': 'italic',
553                },
554            },
555

We can use SCSS-like syntax for hierarchy. '&' are replaced by the parent selector.

558            '&.invalid': { // FancyList.invalid
559                'color': 'red',
560            },
561            '&::after': { // FancyList::after
562                'content': '""',
563                'display': 'block',
564            },
565            'p': { // FancyList p
566                'font-weight': 'bold',
567                'em': { // FancyList p em
568                    'color': 'blue',
569                }
570            }
571        }
572    }
573
574    compose() { /* ... */ }
575
576}
577

Torus also comes with a template tag, css, that makes writing CSS like this less tedioius, by parsing a single block of string into the styles object for you. Using this template tag, FancyList can look like this.

581

Styles defined with the css template tag are cached for render, so for stylesheets that do not change often between renders, writing styles with the template tag may yield performance benefits over defining styles as objects, like above. For styles that change often, however, code in hot paths may perform better with inline styles in JDOM or with styles defined as objects, not through the template tag.

587class FancyList extends StyledComponent {
588
589    styles() {

Write the styles returned as a template literal with css.

591        return css`
592        font-size: 18px;
593        background: rgba(0, 0, 0, .4);
594
595        @keyframes some-name {
596            from {
597                opacity: 0;
598            }
599            to {
600                opacity: 1;
601            }
602        }
603
604        @media (min-width: 600px) {
605            border-color: rgb(0, 1, 2);
606            p {
607                font-style: italic;
608            }
609        }
610
611        &.invalid {
612            color: red;
613        }
614        &::after {
615            content: "";
616            display: block;
617        }
618        p {
619            font-weight: bold;
620            em {
621                color: blue;
622            }
623        }
624        `;
625    }
626
627    compose() { /* ... */ }
628
629}
630

Data models

632

Record

634

Record is how Torus represents a unit of mutable state, whether that's global app state, some user state, or a model fetched from the server. Let's say we want to make a page for a cake shop. We can have a record represent a cake customers can buy.

638class CakeProduct extends Record {}
639

To create an instance of a cake...

641const cake1 = new CakeProduct({
642    // can you tell I know nothing about cakes?
643    name: 'Strawberry Cake',
644    bread: 'wheat',
645    icing: 'strawberry',
646    price_usd: 15.99,
647});

We can also instantiate a record with a unique ID...

649const cake2 = new CakeProduct(2, {
650    name: 'Chocolate Mountain',
651    bread: 'chocolate',
652    icing: 'whipped',
653    price_usd: 13.99
654});
655cake1.id; // null
656cake2.id; // 2
657

To get a property back from a record, use the #get() method

659cake1.get('name');
660// 'Strawberry Cake'
661cake2.get('price_usd');
662// 13.99
663

To update something about the record, call '#update()` with a dictionary of the new properties.

666cake2.update({
667    name: 'Chocolate Volcano',
668    price_usd: 12.99,
669});
670cake2.get('price_usd');
671// 12.99
672

Record#serialize() will return a JSON-serialized version of the state of the record. Normally, this is the same as the summary, but we can override this behavior as appropriate.

675cake1.serialize();
676/*
677    {
678        id: null,
679        name: 'Strawberry Cake',
680        bread: 'wheat',
681        icing: 'strawberry',
682        price_usd: 15.99,
683    }
684*/
685

Record#summarize() returns a "summary" of the state of the record, which is a dictionary of all of its properties, plus its id, even if it's null.

688cake2.summarize();
689/*
690    {
691        id: 2,
692        name: 'Chocolate Mountain',
693        bread: 'chocolate',
694        icing: 'whipped',
695        price_usd: 13.99
696    }
697*/
698

We can have components whose state is bound to a record's state, so when the record updates its properties, the component can respond appropriately. We do this with Component#bind()

701class CakeProductListing extends Component {
702
703    init(cakeProduct) {

Bind this component to cakeProduct, so that when cakeProduct updates, this callback function is called with the summary of the record. Usually, we'll want to re-render. Note that when we bind, it immediately calls the callback (in this case, this.render(props)) once. That means everything we need to run render should be defined before this bind call.

709        this.bind(cakeProduct, props => {

compose() will be called with props, the summary of the cake record. We don't always have to just re-render when the record updates. we can also do other things in this callback. But rendering is most common.

713            this.render(props);
714        });
715    }
716

We can have the component stop listening to a record, by calling #unbind().

718    stopUpdating() { // probably wouldn't write something like this normally
719        this.unbind();
720    }
721

When we bind a component to a new record, it stops listening to the old one automatically.

723    switchCake(newCake)  {
724        // implicit `this.unbind()`
725        this.bind(newCake, props => this.render(props));
726    }
727
728}
729
730const listing = new CakeProductListing(cake1);

We can access the record that a component is bound to, with the record property

732listing.record === cake1; // true
733

Store

735

A store represents a (potentially) ordered collection of records. Used in conjunction with lists below, stores are a good way of organizing records into list-like structures. Components can bind to stores just like they can bind to records.

739

We can create a store by giving it an array of records.

741const cakes = new Store([cake1, cake2]);

Store#records points to a Set instance with all the records in the collection. If you want to check the size of a store, use #records.size. But if you want to access the records in a store, Store#summarize() is a better interface, since it can be pre-sorted based on a comparator.

746cakes.records; // Set({ cake1, cake2 }), a Set
747cakes.records.size; // 2
748cakes.summarize(); // [cake1, cake2], an Array

We can also iterate over a store, like an array or set. This means we can spread ...

750const cakesArray = [...cakes];

...and we can for...of loop over stores. These, like .records, are not necessarily ordered.

752for (const cake of cakes) {
753    do_something_with_cake(cake);
754}

Like records, Store#serialize() returns a JSON representation of its contents. For stores, this means it's an array that contains the serialized JSON form of each of its records, sorted by the comparator (below).

758cakes.serialize(); // [{ /* cake1 props */ }, { /* cake2 props */ }]
759

We can also add to stores later...

761const cakes2 = new Store(); // empty
762cakes2.add(cake1); // returns cake1
763cakes2.add(cake2); // returns cake2
764cakes2.records.size; // 2

... and remove them.

766cakes2.remove(cake1);
767cakes2.summarize(); // [cake2]
768

We can find records in a store by ID, using Store#find()

770cakes2.find(2); // returns cake2, which has id: 2

... it'll return null if the store doesn't contain a match

772cakes2.find(100); // null
773

If we want to refresh the contents of the store with a new set of cakes, we can use #reset(). This will also emit an event to any components listening.

777cakeA = new CakeProduct(/*...*/);
778cakeB = new CakeProduct(/*...*/);
779cakes2.reset([cakeA, cakeB]);
780cakes2.summarize(); // [cakeA, cakeB]
781

In practice, we'll want to define a custom store that knows how to sort the collection in some order, and what record the collection contains. We can extend Store to do this.

785class CakeProductStore extends Store {
786

The recordClass getter tells the Store what this store contains.

789    get recordClass() {
790        return CakeProduct;
791    }
792

The comparator getter returns a function that maps a record to the property it should be sorted by. So if we wanted to sort cakes by their names, we'd write:

796    get comparator() {
797        return cake => cake.get('name');
798    }
799

If you're looking for more advanced sorting behavior, you're welcome to override Store#summarize() in your subclass based on Torus's implementation.

802
803}
804

Since our store now knows what it contains and how to sort it, it can do two more things.

807const cakes = new CakeProductStore([cake1, cake2]);

First, it can create new cakes from its properties

809const fruitCake = cakes.create(3, {
810    name: 'Tropical Fruit',
811    price: 16.49,
812});
813// fruitCake is the newly created cake Record
814cakes.records.size; // 3

Second, it can also sort them by the comparator when providing a summary

816cakes.summarize(); // [cake2, cake1, cake3], sorted by 'name'
817

Rather than overriding get recordClass() each time you subclass Store, Torus provides a StoreOf() shorthand, so you can write the above like this.

820class CakeProductStore extends StoreOf(CakeProduct) {
821
822    get comparator() {
823        return cake => cake.get('name');
824    }
825
826}
827

List

829

80% of user interface development is about building lists, and the best UI platforms like iOS and (as of recently) Android come with great list view primitives like UICollectionView on iOS or RecyclerView on Android. The web doesn't, but Torus comes with a basic list view implementation that works well with Torus components for small lists, up to hundreds of items. (I might build a more performance-focused collection view later.)

836

To define our own list component, we first create a component for each item in the list. Let's say we want to reimplement the RankingsList above as a Torus list component.

840class WinnerListing extends Component {
841
842    init(winner, removeCallback) {

We can store the winner's properties here

844        this.winner = winner;

When we want to remove this item from the list (from a user event, for example), we can run this callback passed from List.

848        this.removeCallback = removeCallback;
849    }
850
851    compose() {
852        return jdom`<li>
853            ${this.winner.name}, from ${this.winner.city}
854        </li>`;
855    }
856
857}

We also need to define a Store for our winners, so we can have something the list can sync its contents with. Let's also give it a fetch() method.

860class WinnerRecord extends Record {}
861class WinnerStore extends StoreOf(WinnerRecord) {
862
863    async fetch() {
864        const data = fetch('/api/v2/rankings').then(r => r.json());
865        const rankings = data.slice(0, this.limit);

When new data comes back, reset the collection with the new list of winners.

868        this.reset(rankings.map(winner => new this.recordClass(winner)));
869    }
870
871};

Our list component extends List, which already implements creating and managing our WinnerListing views from a Store it's given on creation. Note that List is not styled by default. You can extend the styled version of the list component by extending from Styled(List).

876class WinnerList extends List {
877
878    get itemClass() {
879        return WinnerListing;
880    }
881
882}

Create a new store for winners

884const winners = new WinnerStore();

Create a new list view based on the store. This list will now stay in sync with the store's data until it's removed.

887const winnerList = new WinnerList(winners);
888winners.fetch(); // fetch new data, so the list updates
889                 // when we get it back from the server
890

Torus also has a composable, higher-order component called ListOf that allows us to write the above like this instead. This creates and returns a List component with the item component class set to the class we give it.

895const WinnerList = ListOf(WinnerListing); // same as class WinnerList extends ...
896

Like stores, we can iterate over List instances to get an unfiltered, ordered sequence of components inside the list. This means we can e.g. for...of loop over the list's components.

899for (const winnerListing of winnerList) {
900    do_something_with_each_item_view(winnerListing);
901}
902

By default, List will render the children into a <ul></ul>. But we'll usually want to customize that. To do so, we can just override List#compose() with our custom behavior. The List#nodes property always evaluates to a sorted array of the children nodes, so we can use that in our compose method.

908class WinnerList extends ListOf(WinnerListing) {
909
910    compose() {
911        // We want the list to have a header, and a wrapper element.
912        return jdom`<div class="winner-list">
913            <h1>Winners!</h1>
914            <ul>${this.nodes}</ul>
915        </div>`;
916    }
917
918}
919

We can set and remove a filter function that Torus lists will run on each record before rendering that record's listing in the view. Each time we change the filter, the list will automatically re-render. This is useful for searching through or filtering lists rendered with List.

924class MovieView extends Component { /*...*/ }
925const movieList = new (ListOf(MovieView))(movies);
926

Let's only show movies that start with 'A' or 'a'

928movieList.filter(movie => movie.get('name').toLowerCase()[0] === 'a');

Let's switch the filter to movies that are rated above 80% this unsets the last filter, and sets a new filter.

931movieList.filter(movie => movie.get('rating') > 80);

Show all the movies again

933movieList.unfilter();
934

Router

936

Torus comes with a Router that implements basic client-side routing and works well with Torus components. To use Router on your page, just create an instance of it with your routes

940const router = new Router({

We pass in a JSON dictionary of routes when we create a new router. it'll automatically start listening to page navigations.

943    tabPage: '/tab/:tabID/page/:pageNumber',
944    tab: '/tab/:tabID',
945    about: '/about',
946    contact: '/contact',

Router matches routes top-down, so always put your more general routes later.

948    default: '/',
949});
950

When we later create our top-level app view (or whatever view is in charge of routing) we can listen to events from the router and re-render accordingly.

953class App extends Component {
954
955    init(router) {

We bind to a router just like we'd bind to a record or store. the event summary will contain two values in an array: the name of the route, as defined in the router, and the route parameters, like :tabID.

959        this.bind(router, ([name, params]) => {

You can write your routing logic however you like, but I personally found switch cases based on the name of the route to be a good pattern.

963            switch (name) {
964                case 'tabPage':
965                    this.activeTab = params.tabID;
966                    this.activePage = params.pageNumber;
967                    // render the tab and page
968                    break;
969                case 'tab':
970                    this.activeTab = params.tabID;
971                    // render the tab
972                    break;
973                // ...
974                default:
975                    // render the default page
976            }
977        });
978    }
979
980}
981

When we create the app later, we can pass it our page router

983const app = new App(router);
984

The router has a method, #go(), that we can call to navigate to a new route. For example, if we want some method to redirect the page to tab 'shop', page 3, we can just call...

988router.go('/tab/shop/page/3');
989

If you want the new history entry to replace the old one (rather than get added to the history as a new entry), pass the {replace: true} option to the router.go() method.

993router.go('/tab/shop/page/4', {replace: true});
994

We can also write a component that abstracts this away from us

996const RoutedLink = (route, children) => {
997    return jdom`<a onclick="${() => router.go(route)}">${children}</a>`;
998};

... so we can write later in a nav bar, something like this.

1000class NavBar extends Component {
1001
1002    compose() {
1003        return jdom`<nav>
1004            ${[
1005                RoutedLink('/', 'Home'),
1006                RoutedLink('/about', 'About Us'),
1007                RoutedLink('/contact', 'Contact')
1008            ]}
1009        </nav>`;
1010    }
1011
1012}
1013

Conclusions

1015

That's it! That's all of Torus's API. It's pretty small. Torus tries to be simple, lightweight, and extensible, so rather than being opinionated about how to do common things, it provides a set of powerful extensible APIs on top of a simple component and data model system.

1020

I hope you found this page useful, and I hope Torus helps you build something awesome. If you have any questions or come across bugs, please find me on Github or Twitter at @thesephist :) Thanks for checking out Torus!

1024torus.build('πŸš€');
1025