| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Cell is an API-less framework that creates a self-contained & self-driving DOM.
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:
Building a dynamic web app with Cell can be summarized into the following 3 steps:
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.
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>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"
}]
}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>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>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.
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:
<html>
<body>
<h1>Widget</h1>
<div id='widget'></div>
</body>
</html>ಠᴥಠ = {
$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.
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.
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:
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];
}
}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:
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)
}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:
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.
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.
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. |
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:
Let's look at how we can get a handle on an element.
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.
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.
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).
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:
Sometimes you may want to:
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:
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.
| Back | FazBrowse Home | New Git URL |