Introduction
Cell is an API-less framework that creates a self-contained & self-driving DOM.
- No API : Remember that Cell is all about how you structure your logic and NOT about learning how to use some proprietary API. This document will walk you through this.
- Self contained : Each element is its own universe (context) with its own variables and functions.
- Self driving : Each element communicates with outside world via stateless function calls. This means each cell can survive on its own and plug into anything with zero overhead/footprint.
How to use this document
Because of the unique traits mentioned above, learning Cell is mostly about understanding how Cell works, and not about how to use and memorize some API methods, because there is no API.
In fact, you don't even need to read this tutorial if you just understand the 3 rules.
Here's my suggestion:
- First go ahead and head over to the homepage where it explains the 3 rules.
- Head over to the demo page and play around with it.
- Come back to this document to learn more advanced topics.
Concepts
Building a dynamic web app with Cell can be summarized into the following 3 steps:
1. How to create HTML elements from scratch.
1. How to create HTML elements from scratch
Cell creates HTML elements (Cell calls a created element a "Phenotype") from a blueprint (called "Genotype") written in JSON. "Blueprint" sounds like some serious piece of technology but it's nothing more than just an ordinary javascript variable that describes how each HTML element should look and behave.
All you need to do is declare a variable following a simple naming convention. And then Cell takes care of the rest to turn it into an actual HTML element.
Let's start wit 6 special attributes (The only keywords you need to remember when using Cell, ever) you can use to define an element's structure.
Keyword | Explanation |
---|---|
$cell |
A marker to indicate that this is a Cell element and not just a normal Javascript object |
$type |
Dom Element type (Example: div , span , textarea , etc., and "text" for plain text node) |
$text |
Text content inside the element |
$components |
Array of child Genotypes |
$init |
The function that runs automatically after the element is created |
$update |
The function that runs automatically whenever some data stored on the element updates. |
First, let's take a look at $cell
, $type
and $text
.
a. Creating a simple element
When defining a Genotype variable, it really doesn't matter what you name it. To demonstrate this, we'll name it ಠᴥಠ
.
ಠᴥಠ = {
$cell: true,
$type: "span",
$text: "Hello"
}
Creates:
<span>Hello</span>
$cell: true
indicates that this object desribes an HTML element. (If excluded, it will be treated as just an ordinary object and nothing will render)$type
describes the type of a node--in this casespan
.$text
describes the text content of a node--in this caseHello
.
As we already mentioned, the variable name doesn't matter. The only thing that matters is that you include the $cell
key, and Cell will turn that variable into an HTML element.
NOTE: To create a plain text node, you can use
$type: "text"
. For example,
<div class='row'>
<img src="http://localhost/img.jpg"> Plain text
</div>
You would write
Row = {
class: "row",
$components: [{
$type: "img",
src: "http://localhost/img.jpg"
}, {
$type: "text",
$text: "Plain Text"
}]
}
b. Creating nested elements
This time let's take a look at $components
.
$components
is an array that contains genotypes of child elements. We will take the span
we created above and nest it inside a div
element.
ಠᴥಠ = {
$cell: true,
$type: "div",
$components: [
{ $type: "span", $text: "Hello" },
{ $type: "span", $text: "World" }
]
}
Creates:
<div>
<span>Hello</span>
<span>World</span>
</div>
c. Setting DOM attributes
DOM attributes are first class citizens in Cell.
This means that by default all the attributes will map 1:1 to DOM element attributes (unless you're using the 6 special "$"-prefixed keywords from above). Here's an example:
$el = {
$type: "div",
class: "container",
$components: [{
$type: "a",
$text: "Google",
href: "https://www.google.com"
}]
}
Becomes:
<div class='container'>
<a href="https://www.google.com">Google</a>
</div>
d. Setting event handlers
Setting DOM properties is not limited to string attributes, you can even attach native DOM event handlers. Here's an example:
ಠᴥಠ = {
$cell: true,
$type: "button",
$text: "Button",
onclick: function(e){
alert("clicked!");
}
}
creates a button like this:
<button>Button</button>
and attach the onclick
event handler, so it will display an alert "clicked!"
when you click.
e. Injecting a Cell into an exsiting DOM tree
By default all Cell elements get appended to the document <body>
tag in the order they were declared.
However sometimes you may not want to build an entire web page with Cell. You may just want to generate a small component with Cell and inject it into an existing DOM tree. Here's how:
id
on the element you would like to inject into.
1. Set an <html>
<body>
<h1>Widget</h1>
<div id='widget'></div>
</body>
</html>
id
in your Cell definition.
2. Include the same ಠᴥಠ = {
$cell: true,
id: "widget",
$init: function(){
// some initialization
}
}
THis will replace the original <div id='widget'></div>
with your new Cell element.
You can create and inject as many of these components as you want. Just remember to specify a unique id everytime.
2. How to store data and run programs on elements
Concepts
A. Application Context
B. Containerizing Data and Functions
C. Creating Self-driving Cells
A. Application Context
Every element created with Cell is completely self-contained and can survive on its own. In fact, the only thing Cell (the library) does is create this "smart DOM" at the beginning and then goes away. There's no need for a central structure to maintain all the bindings and event handling.
This is possible because each element functions as its own app execution context, by storing its own data and functions under its own context.
Let's look at an example:
ಠᴥಠ = {
$cell: true,
$type: "input",
class: "textfield",
onchange: function(e){
alert("The value is " + this.value + " and the class name is " + this.class);
}
}
This will turn into a textfield that looks like:
<input class="textfield">
The important part here is what's going on inside the onchange
handler.
Notice we're using this.value
to access the textfield's native value attribute, and this.class
to access the class attribute we just defined right above.
this
context.
TLDR: Cell binds all the variable and function's context to the element they're defined on, so you can directly manipulate them by using the B. Containerizing Data and Functions
Now for the most important part: We know how to express the looks, but how do we store data and application directly on the HTML element?
We've seen from above that:
- to define cell's structure we use the 6 reserved attributes
- to define native DOM attributes, we just define them directly (with no prefix).
Now here's the third and the last part to this rule:
To define a data on an element, you use the
_
prefix.
Here's an example:
ಠᴥಠ = {
$type: "button",
$text: "Click me",
_index: 0,
_items: ["Coffee", "Tea", "Sparkling Water", "Soft Drink"]
}
We have successfully defined _index
and _items
on the button
element. This is all! There is no special method to call in order to store data. You simply define it directly, just like you would define an ordinary attribute. Just remember to prefix your variable with _
.
So how do we use it?
Let's try shuffling through this _items
array by incrementing the _index
attribute whenever you click the button. Just remember, all attributes are bound to the element's context, so you can use the this
context to access them.
ಠᴥಠ = {
$type: "button",
$text: "Click me",
_index: 0,
_items: ["Coffee", "Tea", "Sparkling Water", "Soft Drink"],
onclick: function(){
this._index++;
this.$text = this._items[this._index];
}
}
C. Creating Self-Driving Cells
So far we have looked at how each element can be self-contained by storing data and functions on its own context.
But just storing all those data and logic won't do anything on its own. To make each element truly autonomous (Start its own lifecycle and keep running on its own), we need ways to describe how each cell:
- Kickstarts its lifecycle (
$init
) - Reacts to internal and external changes to sustain itself. (
$update
)
1. $init
The $init
function, when defined, gets automatically executed right after the element gets created.
This is useful if you need to apply some dynamic logic on your element at initialization.
For example you can use $init
to apply some 3rd party library, such as jQuery, Select2, Bootstrap, CodeMirror, etc.
Let's first take a static example without $init
.
var Model = [ {value: "AL", caption: "Alabama"}, {value: "WY", caption: "Wyoming"} ]
var Option = function(m){
return { $type: "option", value: m.value, $text: m.caption }
}
var selector = {
$cell: true,
$type: "select",
class: "js-example-basic-single",
$components: Model.map(Option)
}
This will create the following HTML:
<select class="js-example-basic-single">
<option value="AL">Alabama</option>
<option value="WY">Wyoming</option>
</select>
Now let's use $init
to turn this into a select2
component. Just include an $init
function that looks like this:
$init: function(){
$(this).select2()
}
Here's the full code:
var Model = [ {value: "AL", caption: "Alabama"}, {value: "WY", caption: "Wyoming"} ]
var Option = function(m){
return { $type: "option", value: m.value, $text: m.caption }
}
var selector2 = {
$type: "select",
class: "js-example-basic-single",
$init: function(){
$(this).select2() // $init function turns this into Select2!
},
$components: Model.map(Option)
}
2. $update
The $update
function, when defined, gets executed automatically when any data stored on the element gets updated.
To explain this concept, let's try building an app that:
- Fetches a remote API (Github API)
- Store the data under an element when we get the API response.
- Let that "storing" event automatically trigger
$update
, where we render it.
var GithubApp = {
$cell: true,
_items: [],
$init: function(){
this._github();
},
_github: function(){
fetch("https://api.github.com/search/repositories?q=javascript").then(function(res){
return res.json();
}).then(function(res){
this._refresh(res.items);
}.bind(this));
},
_refresh: function(result){
this._items = result;
},
_template: function(item){
return {
$components: [{
$text: item.full_name
}, {
content: item.description ? item.description : ""
}]
};
},
$update: function(){
this.$components = this._items.map(this._template);
}
}
Let's walk through the code step by step.
$cell: true
: We declare the$cell
attribute to declare that this is a cell object. Cell will turn this into an HTML element._items: []
: All variables MUST be declared on the object initially if you want to track them. For example, if you want$update()
to auto trigger whenever_items
updates, you need to declare the_items
key on the object, like we did here.$init()
: When this element gets created, this function will auto-execute. What we want to do is fetch the Github API when this element gets created, so we callthis._github()
here._github()
: This function's job is to fetch from the Github API. We use the browser's fetch API to make a request to Github Trending search API to search for "javascript" (https://api.github.com/search/repositories?q=javascript) When we get the return value, we turn it into JSON (return res.json()
), and then we callthis._refresh()
._refresh()
: This function simply takes the result from the_github()
function and stores it on the_items
array.$update()
: Here's the good stuff happens.$update()
is triggered automatically when_items
gets updated, because we declared_items
on the object at the beginning. We want to update$components
with what we got from Github, so we run a map onthis.__items
withthis._template
and our Github app is updated.
Important!
To auto-trigger $update()
when a "_ variable" updates, you MUST declare it on the object to indicate that this variable needs to be tracked.
Take a look at this code for example:
{
_items: [],
_counter: 0,
_increment: function(){
this._dirty = true;
this._items.push(this._counter++);
},
$update: function(){
this.$components = this._items.map(function(item){ return {$text: item} })
}
}
In this example, we make use of 3 non-function _ variables: _items
, _counter
, and _dirty
.
However since only _items
and _counter
are defined on the object, they are the only variables that will trigger the $update()
function when their values change.
On the other hand, setting this._dirty = true;
won't trigger the $update()
since it's not explicitly declared in the definition therefore not tracked.
D. Summary
Here's the summary of all the attribute types:
Starts with | Explanation | Example |
---|---|---|
"$" | DOM construction metadata Special properties used to construct the node. Updating them directly modifies the DOM. | $cell , $type , $text , $components , $init , $update |
"_" | Custom Variables Declare variables local to your node and use from your app logic. Modifying them doesn't have direct effect on the DOM | _model , _request , etc. Name them whatever you want as long as they start with "_". |
Default | Everything else, by default gets mapped 1:1 to DOM Attributes. Updating them directly modifies the DOM. | style , class , href , onclick , onkeyup , etc. |
3. Inter-Cellular Communication
So far we know how to create elements and how to bring them to life by storing data and running app logic dierctly on them.
Now let's take a look at how to let these cells communicate with one another to create a truly decentralized DOM.
Normally this would be the most confusing part in web app frameworks, but for Cell this is the easiest part, because all we need to do to let cells communicate is:
- Select an element (Simply using native HTML selectors!)
- Call its function
Let's look at how we can get a handle on an element.
A. HTML Selectors
Cell creates vanilla HTML elements. This means we can use the browser's own native API, such as getElementById
, querySelector
, querySelectorAll
, etc.
Here's an example:
ಠᴥಠ = {
$components: [{
$type: "div",
id: "console",
$text: "",
_refresh: function(text){
this.$text = text;
}
}, {
$type: "input",
onkeyup: function(e){
if(e.keyCode === 13) {
document.querySelector("#console")._refresh(this.value);
}
}
}]
}
Notice how we're simply using document.querySelector("#console")
to select the first component, and then calling its _refresh
function. Simple as that. So whatever you type into the input created by Cell will appear in the #console created by Cell.
B. Context Inheritance and Polymorphism
Earlier we discussed how all variables and functions are bound to their container element's context, allowing us to refer to them simply through this
.
This is the default behavior. But there's a second behavior I haven't mentioned yet:
The this
context gets inherited down the DOM tree.
Let me explain what it means.
1. Quick Object Oriented Programming Analogy
Here's an analogy using a typical object oriented programming example:
public class Animal
{
public int age;
public string gender;
}
public class Human extends Animal
{
public string firstName;
public string lastName;
}
var tom = new Human();
tom.age = 1;
tom.gender = "male";
Here we take an Animal
class, and extend it to create a subclass Human
. Because it inherits its parent attributes, we can simply create a Human
instance and access the attributes inherited from the Animal
class (age
and gender
).
2. How Cell implements "context inheritance"
Well, Cell doesn't have any of these classes and objects. Instead you operate by calling stateless functions. So how does Cell implement this type of behavior?
Cell has a concept called "context inheritance". The underlying premise is the same as class inheritance but the implementation is different, and is designed specifically for HTML DOM. Let's look at an example:
{
$cell: true,
$type: "div",
id: "container",
_items: [],
$components: [
{
$type: "input",
onkeyup: function(e){
if(e.keyCode === 13){
this._items.push(this.value);
}
}
}
]
}
Notice how we have _items
declared on the root element (div#container
), but we are able to access it from the onkeyup()
handler of one of its descendants (the input
element).
Basically whenever you refer to an attribute, the element first looks for the variable on itself. If it doesn't find one, it traverses up the DOM tree until it finds the closest ancestor that has that attribute.
This way you can store a model at any level on the DOM tree and retrieve & manipulate it from any of its descendants.
So for example, we can implement the $update()
function on the div#container
and handle all synchronization there, like this:
{
$cell: true,
$type: "div",
id: "container",
_items: [],
$update: function(){
this.querySelector(".list")._refresh();
},
$components: [
{
$type: "input",
onkeyup: function(e){
if(e.keyCode === 13){
this._items.push(this.value);
this.value = "";
}
}
},
{
class: "list",
_refresh: function(){
this.$components = this._items.map(function(item){
return { class: "row", $text: item };
})
}
}
]
}
Here's what's going on:
onkeyup()
inside theinput
element: It checks to see if the keyCode is 13 (enter key), and when the user presses enter, it tries to push its value intothis._items
array (this._items.push(this.value)
). Cell searches for the_items
attribute on theinput
, but it can't find one so it traverses the DOM tree upwards until it finds one atdiv#container
(the root element).- Once the
_items
array is found, we push the value into it. This triggers an$update()
function on thediv#container
because one of its data variables has changed. - The
$update()
function searches for one of its descendants with the selector.list
, and calls_refresh()
function on it (this.querySelector(".list")._refresh()
). - The
div.list
component has a_refresh
function which takesthis._items
array and creates a$components
array from it. Notice how the same inheritance principle applies here.this._items
is not directly defined on thediv.list
element, so it searches upward the DOM tree until it finds one, and uses that value, which happens to be the samethis._items
we set earlier. (We could have just passed around the array as the function's argument, but this was for the sake of explanation).
C. Pass Element as callback argument
Sometimes you may want to:
- Call Element
#callee
's function_doSomething()
from Element#caller
. - When function
_doSomething()
finishes it somehow returns the control back to the element#caller
Of course we could do it like this:
ಠᴥಠ = {
$components: [{
id: "caller",
onclick: function(e){
document.querySelector("#callee")._doSomething();
},
_done: function(response){
console.log(response);
}
}, {
id: "callee",
_doSomething: function(){
// ... do something here...
document.querySelector("#caller")._done(response);
}
}]
}
What's going on:
- When you click on the
#caller
element, it selects the#callee
element and calls_doSomething()
function on it - When
_doSomething()
finishes it selects the originating element and calls its_done()
function to return the value.
This is perfectily feasible but another way to do this is by passing the caller element itself as an argument, so that we have the handle when we're ready to call its _done()
function and we don't have to run the querySelector
command one more time. Here's the same example, using this approach:
ಠᴥಠ = {
$components: [{
id: "caller",
onclick: function(e){
document.querySelector("#callee")._doSomething(this);
},
_done: function(response){
console.log(response);
}
}, {
id: "callee",
_doSomething: function($caller){
// ... do something here...
$caller._done(response);
}
}]
}
Notice that we're passing this
as an argument to _doSomething()
, so that #callee
now has the handle back to the #caller
element.