Janus is a functional, reactive Javascript framework which makes complex user interfaces safe and easy to realize. Modular but opinionated, Janus is built on a strong formal base but provides powerful, familiar building blocks.
Janus is built around a declare-once, work-forever philosophy. It provides easy-to-use but powerful tools for describing data transformations, bindings, and actions. Janus does the work of making sure those declarations—your rules—remain true whenever your data changes. It features:
Templated View Components§
A simple, extensible views and templating library that uses plain HTML and Javascript with familiar, jQuery-like syntax. See examples below.
Collections and Model Library§
Core structures which treat data operations like map
as declarative
transformation rules which should always hold true, rather than as individual
point-in-time mutations. See examples below.
Application Layer§
The tools you need to assemble these components into a full application, including the ability to render server- and client-side with the same codebase.
Below are some small code samples to give you a sense for what Janus looks like. They are simple, but they are fully representative of real-world Janus code.
Views and Templating§
Janus is just plain HTML and Javascript. There are no goofy custom tags, there is no templating language syntax to learn and compile. Instead, we use selectors and a binding syntax based on the jQuery API—things you already understand and rely upon.
const dog = new Map({ name: 'Spot', age: 3 });
const DogTag = DomView.build($(`
<div class="dog-tag">
Hi! I'm <span class="name"/>!
I'm <span class="age"/> years old.
That's <span class="human-age"/> in human years!
</div>`),
template(
find('.name').text(from('name')),
find('.age').text(from('age')),
find('.human-age').text(from('age').map(age => age * 7))
)
);
return new DogTag(dog);
Of course, the previous example isn't very interesting with static data. Here we rig
up a basic Model definition and edit view. The .render()
call looks at the given
object and inserts an appropriate view for it—in this case, from the Janus
standard library:
const Dog = Model.build(
attribute('name', attribute.Text),
attribute('age', attribute.Number)
);
const DogEditor = DomView.build($(`
<div class="dog-editor">
<div class="name"/>
<div class="age"/>
<div class="info"/>
</div>`),
template(
find('.name').render(from.attribute('name')),
find('.age').render(from.attribute('age')),
find('.info').text(from('name').and('age')
.all.map((name, age) => `${name} is ${age * 7} in human years.`))
)
);
const dog = new Dog({ name: 'Spot', age: 3 });
return new DogEditor(dog, { app });
Data Structures and Models§
Janus provides a unique data structure library. All collection transformations
in Janus handle changes over time, so that you don't have to. Here is a simple
example. Here also you get a taste of janus-inspect
, a visual debugging tool:
try editing the values on the source list.
const list = new List([ 4, 8, 15, 16, 23, 42 ]);
const mapped = list.map(x => x * 2);
return [
inspect.panel(list),
inspect(mapped)
];
0 4
1 8
2 15
3 16
4 23
5 42
5 42
Common array operations are all supported, such as .filter()
, as shown here.
And as you can see, changes in the result of the lambda function will result
in automatic changes to the list. Here, we allow named and typed pets, and we
display a list of just the names of dogs.
const Pet = Model.build(
attribute('name', attribute.Text),
attribute('kind', class extends attribute.Enum {
_values() { return [ 'dog', 'cat', 'rabbit', 'hamster', 'iguana', 'parrot' ]; }
}));
const PetEditor = DomView.build(
$('<div class="pet"><span class="name"/><span class="kind"/></div>'),
template(
find('.name').render(from.attribute('name')),
find('.kind').render(from.attribute('kind'))
));
const pets = new List([
new Pet({ name: 'Kenka', kind: 'dog' }),
new Pet({ name: 'Gertrude', kind: 'cat' }),
new Pet({ name: 'Maizie', kind: 'dog' }),
new Pet({ name: 'Widget', kind: 'cat' }),
new Pet({ name: 'Squawks', kind: 'parrot' })
]);
const editors = pets.map(pet => new PetEditor(pet, { app }));
const dogNames = pets
.filter(pet => pet.get('kind').map(kind => kind === 'dog'))
.flatMap(pet => pet.get('name'));
return [ editors, dogNames ];
Janus collections support key enumeration, which can be a powerful tool. Again, all enumerations and traversals can be declared once and changes are automatically maintained. Consider, for instance, a user-defined custom metadata bag:
const widget = new Map({
id: 1138,
name: 'Important Widget',
metadata: {
custom1: 'a way',
custom2: 'a lone',
custom3: 'a last',
custom4: 'a loved',
custom5: 'a long'
}
});
return widget.keys()
.flatMap(key => widget.get(key).map(value => `${key}: ${value}`));
Another example of enumeration involves the shadow-copy feature of Maps and Models. Using enumeration-based traversal, our collections can provide features like advanced serialization, or, as seen here, deep change detection.
const NamedModel = Model.build(attribute('name', attribute.Text));
const model = new NamedModel({
name: 'A model',
child: new NamedModel({ name: 'A child model' })
});
const shadow = model.shadow();
const Editor = DomView.build(
$('<div><div class="name"/><div class="child-name"/></div>'),
template(
find('.name').render(from.attribute('name')),
find('.child-name').render(from('child').attribute('name'))
));
const changed = shadow.modified().map(modified => `Changed: ${modified}`);
return [ new Editor(shadow, { app }), changed ];
A Simple Application§
Here we create the classic Todo list sample. We define a model and a view for
Item
, then render a list of them. Notice how simple the event handling is:
we just manipulate the data. Everything else, including the list management,
falls effortlessly out of our declarative bindings.
const Item = Model.build(
attribute('done', attribute.Boolean),
attribute('description', attribute.Text)
);
const Main = Model.build();
const ItemView = DomView.build($(`
<div class="todo-item">
<span class="done"/>
<span class="description"/>
<button class="remove">x</button>
</div>`), template(
find('.done').render(from.attribute('done')),
find('.description')
.classed('done', from('done'))
.render(from.attribute('description')),
find('.remove').on('click', (event, subject) => { subject.destroy(); })
));
const TodoListView = DomView.build($(`
<div class="todo-list">
<span class="completed"/> of <span class="total"/> items done
<div class="items"/>
<button class="add">Add Item</button>
</div>`), template(
find('.completed').text(from('items').flatMap(items =>
items.filter(item => item.get('done')).length)),
find('.total').text(from('items').flatMap(items => items.length)),
find('.items').render(from('items')),
find('.add').on('click', (event, subject) => {
subject.get_('items').add(new Item());
})
));
const app = new App();
stdlib.view($).registerWith(app.views);
app.views.register(Item, ItemView);
app.views.register(Main, TodoListView);
const view = app.view(new Main({ items: new List() }));
view.wireEvents();
return view;
A fancier, more complete Todo list can be found in the samples repository.
A Closer Look§
For more information about Janus, please take a look at the following resources:
- The introduction walks through the general setup and explains each component we ship in greater detail.
- There are two different introductory guides available. One is a more practical how-to for those who want to just get coding and learn best by doing, while the first principles section builds a deeper picture of the hows and whys from motivations and principles.
- The cookbook is a compendium of common problems and answers that serve as a quick solutions reference, but also illustrate how to approach problem-solving in Janus.
- The Janus Samples repository contains complete example projects that show Janus in use in context.