-
Notifications
You must be signed in to change notification settings - Fork 51
Basic Tutorial: Creating List with Live Filtering
All documentation moved to site:
- Guides: https://github.com/omniscientjs/omniscientjs.github.io/tree/master/_guides
- Tutorials: https://github.com/omniscientjs/omniscientjs.github.io/tree/master/_tutorials
On site:
- Guides: http://omniscientjs.github.io/guides/
- Tutorials: http://omniscientjs.github.io/tutorials/
If you haven't read the Omniscient introduction article yet, you should probably read it before you get started on this tutorial. The previous article in this series introduces the concepts and architecture. In this example we will see how we can use React, Omniscient.js, and Immutable.js a rudimentary search filtering of javascript libraries and frameworks. You can see a running demo on the Omniscient homepage and the complete source code on Github. After we've implemented the example, we'll look into how we can debug an Omniscient application, and see what components get re-rendered.
Note: This tutorial was updated October 29. 2014, to match React v0.12.0
and Omniscient v2.0.0
. See the tutorial for React pre v0.12
and Omniscient pre v2.0.0
For this example, we're creating a small application where we can search for different javascript libraries and frameworks. We should start by requiring the modules we want to use. A basic stack with Omniscient is React, immstruct and of course Omniscient (amd Immutable.js wrapped through immstruct).
var React = require('react'),
immstruct = require('immstruct'),
component = require('omniscient');
To show a list of different javascript projects, we should have a top structure with our data defined as a immutable structure. We are creating a list of different project and their URLs, but also a empty string with search
. This will be the current search query, that we want as an active filter. The entire application state is defined in that structure alone. If we wanted, we could have started the application with a predefined search – making it easier to test and demo.
var structure = immstruct({
search: '',
libs: [
{ title: 'Backbone.js', url: 'http://documentcloud.github.io/backbone/' },
{ title: 'AngularJS', url: 'https://angularjs.org/' },
{ title: 'jQuery', url: 'http://jquery.com/' },
{ title: 'Prototype', url: 'http://www.prototypejs.org/' },
{ title: 'React', url: 'http://facebook.github.io/react/' },
{ title: 'Omniscient', url: 'https://github.com/omniscientjs/omniscient' },
{ title: 'Ember', url: 'http://emberjs.com/' },
{ title: 'Knockout.js', url: 'http://knockoutjs.com/' },
{ title: 'Dojo', url: 'http://dojotoolkit.org/' },
{ title: 'Mootools', url: 'http://mootools.net/' },
{ title: 'Underscore', url: 'http://documentcloud.github.io/underscore/' },
{ title: 'Lodash', url: 'http://lodash.com/' },
{ title: 'Moment', url: 'http://momentjs.com/' },
{ title: 'Express', url: 'http://expressjs.com/' },
{ title: 'Koa', url: 'http://koajs.com' },
]
});
We could also load the data lazily. This is a operation that shouldn't be a part of our UI, but a separate module handling models and collections. We could send a cursor to where we want the data populated using Immutable.js. For instance:
var storage = require('./storage');
var structure = immstruct({
search: '',
libs: [ ]
});
// This uses AJAX and swaps the empty libs list
// in our structure to a new one with the new models.
storage.fetchLibraries(structure.cursor('libs'));
There would't be any need for doing anything else. If our fetchLibraries
method swapped the value in our structure, we would get an event from immstruct
telling us that a value has been swapped in the data structure. We should re-render if this happens.
render();
structure.on('swap', render);
function render () {
React.render(
Search(structure.cursor()),
document.body
);
}
Remember, structure
is the immutable structure we created, and it emits event, swap
, using immstruct, when this event is emitted, we want to re-render the entire component tree, starting with a top parent component. We'll call it Search
in this example.
Now we us start implementing Search
, which will consist of a SearchBox
and a set of Matches
. This is pretty straight forward, we are simply making HTML elements and describing our view in a declarative way using components. Much like doing markup, but in Javascript.
var Search = component('Search', function (props) {
return React.DOM.div({ },
SearchBox(props.cursor.cursor('search')),
Matches(props.cursor));
});
As we see, a component can be created by using Omniscient. The first argument is a string representation of the component and is used for debugging, as well as Component name for React. The second argument is the component implementation it self as a function. This function is a render function. When a component should render this function is executed, and when it's return value will be the presentation of this component. The render function of a component is passed cursor, which is the cursor a parent component or the top renderer is serving. In our case we, Search
will get passed an object literal with a property of our cursor to the entire application state through structure.cursor()
as defined in our render
function above.
As we see, Search
, this is a small component that only outputs a division with two sub-components; SearchBox
and Matches
. The empty object as first argument to the React div-element is HTML metadata to the div. We see that Matches
gets the same cursor as Search
has (a cursor to the entire structure), as it is a representation of the entire state, but the SearchBox
only need information about the search query. It should't concern it self with how the list is stored or handled.
The next natural step is to list out all the matches based on the search query - as is the main part of our application. We don't have any way of altering the search query yet, but that doesn't matter. We have the search query as a value inside our global application state, and with it being empty, we should list out all our javascript projects.
A list of matches in turn consist of a single Match
. This will be a component that merely presents a javascript library as a list item with an anchor-element, like so:
var Match = component('Match', function (props) {
var cursor = props.cursor;
return d.li({}, d.a({ href: cursor.get('url') }, cursor.get('title')));
});
The Matches
component is far more interesting, and is the heart of the application. We have to get the search query and all projects, and we have to filter the projects based on that query. The matches should be presented to the browser in a un-ordered list of elements.
var Matches = component('Matches', function (props) {
// get our cursor from the properties.
var cursor = props.cursor;
// Get the value from search query
var q = cursor.get('search');
// Get all projects
var libs = cursor.get('libs');
// Get all javascript projects that matches the query
var matches = libs.filter(function (lib) {
return lib.get('title').indexOf(q) !== -1 || lib.get('url').indexOf(q) !== -1;
});
// Present the matches
return React.DOM.ul({}, matches.toArray().map(function (lib, i) {
return Match('match-' + lib.get('title'), lib);
}));
});
Even this, the main part of our application, shouldn't do anything that is not related to the presentation. We only want to present the items that match our search query, so we filter the list of libraries and only select those that contains our search query, either in the name or the URL. Now we actually have a working filterable list of javascript projects, we just don't have a way of filtering. But if we try to change our initial search query like so:
var structure = immstruct({
search: 'Omniscient',
// ...
});
... and refresh the browser, we would see only Omniscient being presented in the list. And every time we refresh the browser, this is what we see. Pure, predictable components. But altering the source code and refreshing the browser isn't really user friendly - or fast. We should create a own separate component for updating the search query.
var SearchBox = component('SearchBox', function (props) {
return React.DOM.div({}, d.input({
placeholder: "Search..",
value: props.cursor.deref(),
onChange: this.changeHandler
}));
});
The SearchBox
is easy, but we see two things here that might be new. The cursor we get passed is a cursor directly to the search string. This means that to get the string we need to de-reference the cursor by calling props.cursor.deref()
. By having the value as the de-referenced cursor we have a input box with the search query text. Another thing here is the changeHandler
. We have seen a event handler previously, but not the onChange
. onChange
is triggered every time the input is changed (not blurred as one might expect).
We create a small mixin that can handle the change for us. As this is javascript and we can share a context with the component through this
. The passed cursor is available through the property this.props.cursor
inside the mixin, and the cursor points directly to the search query in the structure.
var mixins = {
changeHandler: function (e) {
this.props.cursor.update(function (currentSearch) {
return e.currentTarget.value;
});
}
};
// Change our SearchBox from above to add the mixins
var SearchBox = component('SearchBox', mixins, function (props) { /* same as before */ });
The changeHandler
has one small job: update the cursor with the new value of the input box. This will swap the search query in the immutable structure and through immstruct tell the application the do the re-render, as immstruct emits an event and we listen to that event and trigger render. That is it. That is our entire live filter application. It is pretty awesome and really easy to reason about - almost as simple as HTML it self. But let's think a bit about what is happening here. What happens when we re-render, and how do we debug.
Check out the complete source code on Github.
The re-render is interesting here. We will go through, top-down, see if the components have changed. So when a new input is registered in our text field, we swap out the search query value in our structure and trigger a re-render. When the re-render happens, we see if the cursor passed to Search
has changed, where we do:
React.render(
Search(structure.cursor()),
document.body
);
It has changed, as a part of the object has changed. So we do a re-render. The component function is executed, and we again see if SearchBox
and Matches
has changed. Both have changed, as both have access to the search query, so we trigger a re-render on both of them. The search box simply renders out the new updated state to the browser, setting input value, but the Matches
how ever, generates a new list of sub-components, by filtering all javascript projects based on the search query.
If we activate the Omniscient debugger, we can see what happens next:
// Activate debug-modus
component.debug();
While having the debug-mode activated, we try to go from e
to en
in the input box. We should get 4 results. The debug output is
<Search>: shouldComponentUpdate => true (cursors have changed)
<Search>: render
<SearchBox>: shouldComponentUpdate => true (cursors have changed)
<SearchBox>: render
<Matches>: shouldComponentUpdate => true (cursors have changed)
<Matches>: render
<Match key=match-Backbone.js>: shouldComponentUpdate => false
<Match key=match-Omniscient>: shouldComponentUpdate => false
<Match key=match-Underscore>: shouldComponentUpdate => false
<Match key=match-Moment>: shouldComponentUpdate => false
We see that all the parent components and the SearchBox are re-rendered. What is interesting is that none the Match
components isn't re-rendering. Matches
re-renders, and removes several elements that doesn't match our query. The libraries that do match our query, is outputed but they havent changed, so they don't re-render - they just remain from the previous render.
If we remove a letter from the search query (making it e
again), the debug-output for the Match
component show:
<Match key=match-Backbone.js>: shouldComponentUpdate => false
<Match key=match-jQuery>: render
<Match key=match-Prototype>: render
<Match key=match-React>: render
<Match key=match-Omniscient>: shouldComponentUpdate => false
<Match key=match-Ember>: render
<Match key=match-Mootools>: render
<Match key=match-Underscore>: shouldComponentUpdate => false
<Match key=match-Moment>: shouldComponentUpdate => false
<Match key=match-Express>: render
We can see that all the components that matched the previous query are still unchanged, and only the new components get rendered.
This has been a introductory tutorial for how we can use Omniscient and React to make applications. The application we created in this tutorial, was fairly naïve and doesn't face the problems of real life. It is very rare we can have code as clean and small as this, but this shows, at the most basic level, how we can reason about a component based, top-down rendered, UI. It can be challenging at first, to switch out our "normal" way of designing Javascript software, and having a loop that iterates everytime we make a small change. It also might seem like this would be slow and un-optimized, but the smart implementation of React and the reference checks of Omniscient and Immutable.js will actually allow us to create fast, responsive, applications. You can see more example source codes on Github or try them out on the Omniscient homepage. On the Omniscient homepage, you can also see how the immutable structure state is changing over the course of the application life time.
If you have any comments on this architecture, think this is a horrible idea, or simply have questions about Omniscient, you can reach out on Github issues or ping us at Twitter: @mikaelbrevik and @torgeir.