A simple library for building Component-based Web user interfaces.
Note: This is an educational project (~1000 lines of code). There are many libraries doing this for professional projects (e.g: Ember, AngularJS, Vue.js, React, etc.)
This readme and documentation can be also viewed in https://lipido.github.io/fronty.js.
Models are Observable objects,
that extend the Model
class. Models contain your application's "logic
state" (e.g.: list of todo items, current editing employee, current logged user,
etc.).
In order to make changes in a model, you have to use the set
method, which
will notify all observers that a change has been made.
var myModel = new Fronty.Model('mymodel');
myModel.counter = 0;
// update the model later
myModel.set( () => myModel.counter++ );
Renderers allows you to maintain your HTML separated from your JavaScript code. A renderer is any function that returns an HTML string.
A special type of renderers are those that take a model and converts it into
HTML (see ModelComponent
, afterwards). A very powerful library to create this
function is Handlebars, since a Handlebars template
is a valid renderer function for fronty.js (you can find many
more). For example:
<div>
<span>Current counter: {{counter}}</span>
<button id="increase">Increase</button>
</div>
If you compile this template with Handlebars, you get a valid renderer function
that would be able to render the previous model, where counter
is a property
of the model.
Note: renderers MUST return a piece of HTML with a single root element.
Components take a renderer function and puts its resulting HTML in the actual
and visible document by making as less changes as possible in the document
tree in order to increase performance and preserve interactive element's
status (such as form input elements). A component is rendered in place of a
given HTML element identified by its id
, so everything inside that node is
responsibility of the component. Components can be re-rendered at any time so,
if the renderer function returns a different content, the component will make
the necessary changes in the current HTML.
The most typical Component
is ModelComponent
, which receive a Model
, and
a renderer function able to take a model and generate HTML (e.g: a compiled
Handlebars template). The component observes the model. If any change is made
in the model, the component will re-render.
var myModel = new Fronty.Model('mymodel');
myModel.counter = 0;
var aTemplate = Handlebars.compile(
'<div><span>Current counter: {{counter}}</span><button id="increase">Increase</button></div>'
);
var myComponent = new Fronty.ModelComponent(aTemplate, myModel, 'myapp');
In the example, the component will be placed inside the element with
id="myapp"
.
In addition, you add event listeners to components (not directly to HTML nodes):
myComponent.addEventListener('click', '#increase', () => {
//update the model
myModel.set( () => myModel.counter++ );
});
Finally, components do not render until you call start()
.
myComponent.start();
Another special component is the RouterComponent
, a class that is able to
simulate "multiple-pages" inside a single-page application by using the hash
part of the current url.
Here you have a single page with a minimal code to see fronty.js working.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.min.js"></script>
<script src="js/fronty.js"></script>
</head>
<body>
<div id ="myapp">Loading</div>
<script>
// Model
var myModel = new Fronty.Model('mymodel');
myModel.counter = 0;
// Template
var aTemplate = Handlebars.compile(
'<div><span>Current counter: {{counter}}</span><button id="increase">Increase</button></div>'
);
// Component
var myComponent = new Fronty.ModelComponent(aTemplate, myModel, 'myapp');
myComponent.addEventListener('click', '#increase', () => {
//update the model
myModel.set( () => myModel.counter++ );
});
// Start rendering
myComponent.start();
</script>
</body>
</html>
This is a minimal example. The next thing should separate the templates from the JavaScript. You can:
-
Precompile your templates (best performance).
-
Retrieve this template from an external file (
templates/counter-template.hbs
) by using AJAX, compile it, and then pass it to the component. The following code is an example:
// a helper function to load an external text file
function loadTextFile(url) {
return new Promise((resolve, reject) => {
$.get({
url: url,
cache: true,
dataType: 'text'
}).then((source) => {
resolve(source);
}).fail(() => reject());
});
}
Handlebars.templates = {};
Promise.all([
// here we retrieve our template files, you can put here all the templates
loadTextFile('templates/counter-template.hbs').then((source) => Handlebars.templates.counter = Handlebars.compile(source))
])
.then(() => {
$(() => {
// once templates are loaded and the document is ready, it is safe to start
var myModel = new Fronty.Model('mymodel');
myModel.counter = 0;
var myComponent = new Fronty.ModelComponent(Handlebars.templates.counter, myModel, 'myapp');
myComponent.addEventListener('click', '#increase', () => {
//update the model
myModel.set( () => myModel.counter++ );
});
myComponent.start();
});
}).catch((err) => {
alert('FATAL: could not start app ' + err);
});
The template/counter-template.hbs
would be:
<span>Current counter: {{counter}}</span><button id="increase">Increase</button>
Models and Components are classes (ECMAScript 2015). It is recommended that you build your own Models and Components by extending these classes.
For example:
class Counter extends Fronty.Model {
constructor() {
super('counter');
this.counter = 0;
}
increase() {
this.set( () => { this.counter++ });
}
}
class CounterComponent extends Fronty.ModelComponent {
constructor(counterModel, node) {
super(Handlebars.templates.counter, counterModel, node);
this.counterModel = counterModel;
this.addEventListener('click', '#increase', () => {
//update the model
this.counterModel.increase();
});
}
}
Components can also be composed one inside other for better modularity,
reusability and performance by using
component.addChildComponent(childComponent)
. You can also place a special attribute
in parent components to create child components dynamically. For example:
<!-- parent template -->
<ul>
{{#each items}}
<li fronty-component="TodoItemComponent" id="item-{{id}}" key="item-{{id}}" model="items[{{@index}}]"></li>
{{/each}}
</ul>
<!-- child template -->
<li key="item-{{id}}">
{{description}}
</li>
// parent component
class TodoListComponent extends Fronty.ModelComponent {
constructor(id, items) {
super(
Handlebars.compile(document.getElementById('todo-list-template').innerHTML),
items, id);
}
}
// child items component
class TodoItemComponent extends Fronty.ModelComponent {
constructor(id, item) { // <--- a component class with this constructor must be available
}
}
In the parent component, you have to indicate the fronty-component
attribute
to create child components dynamically, and pass the
model that the expression found in the attribute model
evaluates to.
If you want to instantiate the child components by hand, you can override the
createChildModelComponent(className, element, id, modelItem)
function in the parent
component. For example:
class TodoListComponent extends Fronty.Component {
constructor(id, items) {
super(
Handlebars.compile(document.getElementById('todo-list-template').innerHTML),
items, id);
}
createChildModelComponent(className, element, id, modelItem) {
if (className === 'TodoItemComponent') {
return new TodoItemComponent(itemId, modelItem);
}
}
}
See an example in here.
Note: If you use a module system for JavaScript, Fronty will not be able to locate your class, so it is mandatory to override the method like in this example.
The next figure shows a class diagram with the classes of the framework. For a complete description of the API see https://lipido.github.io/fronty.js
- One-way binding. Changes in models are reflected in HTML, but changes in HTML interactive elements are not reflected in models automatically.
- Component-based. Each part of the DOM is rendered by a component. Components are nestable.
- No third-party libraries required.
- Template engine agnostic. Tested with Handlebars.
- Updates the DOM by diff+patch (similar to "Reconciliation" in React).
- Models are mutable and observed by components. Only those ModelComponent objects that observe a changing model are re-rendered, so you can control which part of the DOM is re-evaluated for changes.
- What about performance? I have benchmarked Fronty using a well-known benchmark. Results can be seen here.