Overview
Components are the building blocks of Derby applications. A component is a view associated with a controller class. The view is implemented as a Derby template and the controller is implemented as a JavaScript class or constructor function. Derby creates an instance of the controller class each time it renders the component view.
Reuse and organization
Components are reusable UI pieces, similar to custom HTML elements. In addition, they are the recommended way to structure complex applications as modular parts with clear inputs and outputs. Each significant unit of UI functionality should be its own component.
Components can be rendered on the server and the client, so the same code can produce static HTML, server-rendered dynamic applications, and client-rendered applications.
Encapsulation
Each component has a scoped model in its own namespace. Data or references to the component’s parent are passed in via view attributes. If you’re familiar with it, this structure is similar to the Model-View-ViewModel (MVVM) pattern—a component’s scoped model is a ViewModel.
Tabs Example
index.html
<Body:>
<view is="tabs">
<pane title="One">
<p>Some stuff here</p>
</pane>
<pane title="Two">
<p>More stuff</p>
</pane>
</view>
tabs.html
<index: arrays="pane">
<ul class="tabs-nav">
{{each @pane as #pane, #i}}
{{with #i === selectedIndex as #isActive}}
<li class="{{if #isActive}}active{{/if}}">
{{if #isActive}}
<b>{{#pane.title}}</b>
{{else}}
<a on-click="select(#i)">{{#pane.title}}</a>
{{/if}}
</li>
{{/with}}
{{/each}}
</ul>
{{each @pane as #pane, #i}}
<div class="tabs-pane {{if #i === selectedIndex}}active{{/if}}">{{#pane.content}}</div>
{{/each}}
tabs.js
module.exports = Tabs;
function Tabs() {}
Tabs.view = __dirname + '/tabs.html';
Tabs.DataConstructor = function() {
this.selectedIndex = 0;
};
Tabs.prototype.select = function(index) {
this.model.set('selectedIndex', index);
};
tabs.ts
const Component = require('derby').Component;
export = Tabs;
class TabsData {
selectedIndex: number = 0;
}
class Tabs extends Component<TabsData> {
static view = __dirname + '/tabs.html';
static DataConstructor = TabsData;
selectedIndex = this.model.at('selectedIndex');
select(index: number): void {
this.selectedIndex.set(index);
}
}
tabs.coffee
module.exports = class Tabs
@view: __dirname + '/tabs.html'
@DataConstructor: ->
@selectedIndex = 0
select: (index) ->
@model.set 'selectedIndex', index
(The above example uses derby-standalone, a client-side only build of Derby.)
Todos example
<Body:>
<view
is="todos-new"
on-submit="list.add()">
</view>
<view
is="todos-list"
as="list"
items="{{_page.items}}">
</view>
{{if _page.items.length}}
<view
is="todos-footer"
items="{{_page.items}}">
</view>
{{/if}}
<todos-new:>
<form on-submit="submit()">
<input type="text" value="{{value}}">
<button type="submit">Add todo</button>
</form>
<todos-list:>
<ul class="todos-list">
{{each items as #item, #i}}
<li>
<label class="{{if #item.done}}done{{/if}}">
<input type="checkbox" checked="{{#item.done}}">
{{#item.text}}
</label>
<button type="button" on-click="remove(#i)">Delete</button>
</li>
{{/each}}
</ul>
<todos-footer:>
<div class="footer">
{{remaining(@items)}} items left
</div>
app.component('todos-new', class TodosNew {
submit() {
const value = this.model.del('value');
this.emit('submit', value);
}
});
app.component('todos-list', class TodosList {
add(text) {
if (!text) return;
this.model.push('items', {text});
}
remove(index) {
this.model.remove('items', index);
}
});
app.component('todos-footer', class TodosFooter {
static singleton = true;
remaining(items) {
if (!items) return 0;
return items.filter(item => !item.done).length;
}
});