├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CNAME ├── CONTRIBUTING.md ├── GENESIS.md ├── LICENSE ├── README.md ├── VIRUS.md ├── cell.js ├── examples └── virtual_dom.js ├── index.html ├── package.json ├── phd.config.js ├── test ├── Gene.js ├── Genotype.js ├── God.js ├── Membrane.js ├── Nucleus.js ├── Phenotype.js ├── integration.js └── spy.js ├── travis └── test.sh └── website ├── components └── mailchimp │ └── form.html ├── demos ├── bitcoin.js ├── twitter.css └── twitter.js └── style.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 5 5 | }, 6 | "env": { 7 | "es6": true, 8 | "node": true, 9 | "browser": true 10 | }, 11 | "rules": { 12 | "no-await-in-loop": "warn", 13 | "no-compare-neg-zero": "error", 14 | "no-extra-parens": ["warn", "all", { 15 | "nestedBinaryExpressions": false 16 | }], 17 | "no-template-curly-in-string": "error", 18 | "no-unsafe-negation": "error", 19 | "valid-jsdoc": ["error", { 20 | "requireReturn": false, 21 | "requireReturnDescription": false, 22 | "prefer": { 23 | "return": "returns", 24 | "arg": "param" 25 | }, 26 | "preferType": { 27 | "String": "string", 28 | "Number": "number", 29 | "Boolean": "boolean", 30 | "Symbol": "symbol", 31 | "object": "Object", 32 | "function": "Function", 33 | "array": "Array", 34 | "date": "Date", 35 | "error": "Error", 36 | "null": "void" 37 | } 38 | }], 39 | 40 | "accessor-pairs": "warn", 41 | "array-callback-return": "error", 42 | "complexity": "warn", 43 | "curly": ["error", "multi-line", "consistent"], 44 | "dot-location": ["error", "property"], 45 | "dot-notation": "error", 46 | "eqeqeq": "error", 47 | "no-empty-function": "error", 48 | "no-floating-decimal": "error", 49 | "no-implied-eval": "error", 50 | "no-invalid-this": "error", 51 | "no-lone-blocks": "error", 52 | "no-multi-spaces": "error", 53 | "no-new-func": "error", 54 | "no-new-wrappers": "error", 55 | "no-new": "error", 56 | "no-octal-escape": "error", 57 | "no-return-assign": "error", 58 | "no-return-await": "error", 59 | "no-self-compare": "error", 60 | "no-sequences": "error", 61 | "no-throw-literal": "error", 62 | "no-unmodified-loop-condition": "error", 63 | "no-unused-expressions": "error", 64 | "no-useless-call": "error", 65 | "no-useless-concat": "error", 66 | "no-useless-escape": "error", 67 | "no-useless-return": "error", 68 | "no-void": "error", 69 | "no-warning-comments": "warn", 70 | "prefer-promise-reject-errors": "error", 71 | "require-await": "warn", 72 | "wrap-iife": "error", 73 | "yoda": "error", 74 | 75 | "no-label-var": "error", 76 | "no-undef-init": "error", 77 | 78 | "callback-return": "error", 79 | "handle-callback-err": "error", 80 | 81 | "array-bracket-spacing": "error", 82 | "block-spacing": "error", 83 | "brace-style": ["error", "1tbs", { "allowSingleLine": true }], 84 | "comma-dangle": ["error", "always-multiline"], 85 | "comma-spacing": "error", 86 | "comma-style": "error", 87 | "computed-property-spacing": "error", 88 | "eol-last": "error", 89 | "func-name-matching": "error", 90 | "indent": ["error", 2, { "SwitchCase": 1 }], 91 | "key-spacing": "error", 92 | "keyword-spacing": ["error"], 93 | "max-depth": "error", 94 | "max-nested-callbacks": ["error", { "max": 4 }], 95 | "max-statements-per-line": ["error", { "max": 2 }], 96 | "new-cap": "off", 97 | "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 3 }], 98 | "no-array-constructor": "error", 99 | "no-mixed-operators": "error", 100 | "no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], 101 | "no-new-object": "error", 102 | "no-spaced-func": "error", 103 | "no-trailing-spaces": "error", 104 | "no-unneeded-ternary": "error", 105 | "no-whitespace-before-property": "error", 106 | "nonblock-statement-body-position": "error", 107 | "object-curly-spacing": ["error", "always"], 108 | "operator-assignment": "error", 109 | "operator-linebreak": ["error", "after"], 110 | "padded-blocks": ["error", "never"], 111 | "quote-props": ["error", "as-needed"], 112 | "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": false }], 113 | "semi-spacing": "error", 114 | "semi": "error", 115 | "space-before-blocks": "error", 116 | "space-before-function-paren": ["error", "never"], 117 | "space-in-parens": "error", 118 | "space-unary-ops": "error", 119 | "template-tag-spacing": "error", 120 | "unicode-bom": "error" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | **/.DS_Store 4 | coverage 5 | .nyc_output 6 | index.html 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | cache: 5 | directories: 6 | - node_modules 7 | install: npm install 8 | jobs: 9 | include: 10 | - stage: test 11 | script: bash ./travis/test.sh 12 | dist: trusty 13 | sudo: false 14 | after_success: npm run coverage 15 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | www.celljs.org -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributions Welcome 2 | 3 | Feel free to make suggestions and share improvements on the [issues](https://github.com/intercellular/cell/issues/new). 4 | 5 | # Cell Architecture 6 | 7 | For a quick overview of how Cell works internally, please refer to the [Genesis](./GENESIS.md) document, where it explains every module in detail. 8 | 9 | # Tests Required! 10 | 11 | Because Cell creates HTML elements that are completely self-driving, it can be tricky at times to debug when something goes wrong. 12 | 13 | That's why it is necessary to write as many tests as possible, in order to make sure one fix or improvement doesn't result in unexpected trouble elsewhere. 14 | 15 | So when you do make a contribution, please write a test verifing that: 16 | 17 | 1. Your code does what you say it does. 18 | 2. Your code doesn't break any of the existing tests. 19 | 20 | You can check out all the existing tests under the [/test](./test) folder. 21 | -------------------------------------------------------------------------------- /GENESIS.md: -------------------------------------------------------------------------------- 1 | # Genesis 2 | 3 | ## 1. God 4 | 5 | 1. In the beginning God creates a [Membrane](#2-membrane). Now the cell has a "shell" (an empty html node) that can be filled. 6 | 2. And God builds a [Genotype](#3-genotype). Genotype stores all the data a cell needs to construct itself. 7 | 3. And God builds a [Nucleus](#5-nucleus). Nucleus is the central processing unit of a cell. It handles the cell cycle, synchronization, app execution, and other core functions. 8 | 4. And God builds a [Phenotype](#6-phenotype). Phenotype is the actual manifestation of the cell's genotype as an HTML element. 9 | 5. Finally God's job has finished. From here on, God doesn't get in the way and let each cell take care of its own destiny. Each cell starts its own life cycle based on their membrane, genotype, nucleus, and phenotype. 10 | 11 |
12 | 13 | ## 2. Membrane 14 | 15 | Membrane is the "shell" of a cell. The membrane unit determines whether the cell will be created from scratch or if it will be injected into an existing element on the DOM. 16 | 17 | - `inject()` - Injects a cell into an existing element. You can inject cells into `head`, `body`, or any element with an `id`. 18 | - `create()` - Creates membrane from scratch and appends it to body. In most cases you will just create everything from scratch instead of injecting into an existing DOM. 19 | - `build()` - A wrapper around inject() and create(). Depending on the gene info, it decides whether to inject or create. (Inject in case the `$type` is either `body` or `head`, or if there exists an element on the DOM tree that matches one of the gene ids. Otherwise create and append to body). 20 |
21 | 22 | ## 3. Genotype 23 | 24 | Genotype stores an entire blueprint of a cell. Then, it's used to generate the phenotype (an actual HTML element) 25 | 26 | - `set` - stores a single key/value pair under genotype 27 | - `update` - updates genotype for a single key 28 | - `build` - builds an entire genotype for a node 29 | 30 |
31 | 32 | ## 4. Gene 33 | 34 | Gene is a utility unit that deals with comparing and deduplicating gene data 35 | 36 | - `freeze` - freezes a gene for comparison 37 | - `LCS` - longest common subsequence algorithm 38 | - `diff` - compares two genotypes and comes up with a diff 39 | 40 |
41 | 42 | ## 5. Nucleus 43 | 44 | Nucleus handles the actual cell cycle. Nucleus functions as the interface between the outside world/the programmer and the cell's Genotype and Phenotype. 45 | 46 | - `tick()` - A polyfill method for `requestAnimationFrame`, which is used throughout the cell cycle. Makes sure all the view updates are carried out in a single animation frame. 47 | - `set()` - Instead of directly setting attributes on an element, we use the nucleus structure as a pseudo proxy. This function makes sure that all the attributes defined on the genotype object gets monitored for change, so we can trigger `$update()` whenever there's an update 48 | - `build()` - The root method for building out the nucleus of an element. 49 | - `bind()` - binds the functions so we can run post-processing logic after each function is run, as well as trigger `$update()` when there's an update. 50 | - `queue()` - queues up all the attributes that may have been udpated, so we can check later and make an update all at once when the call stack becomes empty. 51 | 52 |
53 | 54 | ## 6. Phenotype 55 | 56 | A cell's Genotype manifests itself into Phenotype--an actual HTML element. 57 | 58 | - `build()` - builds phenotype for a node from genotype. Internally, callse the `update()` for each gene 59 | - `update()` - updates phenotype for a single gene 60 | - `$type()` - updates the `$type` of a phenotype 61 | - `$components()` - updates the `$components` of a phenotype 62 | - `$init()` - automatically called after `Phenotype.build()` 63 | - `$update()` - automatically called when there's a data update on this cell. 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 gliechtenstein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | GitHub 5 | Demo 6 | Tutorial 7 | Twitter 8 | Slack 9 | 10 |

11 | 12 | Build Status 13 | Coverage Status 14 | 15 |
16 | 17 |
18 | 19 | 20 | # Cell 21 | 22 | A self-constructing web app framework powered by a self-driving DOM. 23 | 24 | 25 | 1. [Philosophy](#philosophy) 26 | 2. [Try Now](#try-now) 27 | 3. [How is it different?](#there-is-no-framework) 28 | 4. [Rules](#there-are-only-3-rules) 29 | 5. [How does it work?](#how-it-works) 30 | 6. [What problems does it solve?](#what-problems-this-solves) 31 | 32 | 33 |
34 | 35 | # Philosophy 36 | 37 | Cell has one and only one design goal: **Easy**. 38 | 39 | 1. **Easy to learn:** There is NO API to learn. You just need to remember 3 rules. 40 | 2. **Easy to use:** You just need a single HTML file with a single ` 60 | 93 | 94 | ``` 95 | 96 |
97 | 98 | 99 | Here's the generated DOM tree, as viewed in Chrome inspector: 100 | 101 | 102 | ![autonomous dom](https://s3-us-west-2.amazonaws.com/fm.ethan.jason/autnonomous_dom.png) 103 | 104 |
105 | 106 | 107 | # There Is No Framework 108 | 109 | A couple of things to note from the code: 110 | 111 | 1. There are no framework classes to inherit and extend from. 112 | 2. There are no API method calls. 113 | 3. There are no HTML body tags. 114 | 4. All we have is a single JSON-like variable. 115 | 5. The DOM just builds itself without you running any function. 116 | 117 |
118 | 119 | # There are only 3 rules 120 | 121 | Cell has no API. 100% of your code will be vanilla Javascript, and there is no framework method or class to implement. 122 | 123 | To use Cell, you simply define a variable that describes the DOM content and behavior. 124 | 125 | **When you follow the 3 rules below, Cell turns it into HTML.** 126 | 127 |
128 | 129 | ## Rule #1. Attributes map 1:1 to DOM attributes by default. 130 | 131 | When you define a Javascript object, its attributes map 1:1 to DOM attributes. So, 132 | 133 | ```js 134 | var node = { 135 | id: "container", 136 | class: "red" 137 | } 138 | ``` 139 | 140 | maps to: 141 | 142 | ```html 143 |
144 | ``` 145 | 146 |
147 | 148 | ## Rule #2. Use 7 special keywords to declare the cell structure 149 | 150 | Key | Description 151 | -------------|--------------------------------- 152 | $cell | Required. Tells Cell to create a cell element using this object as a root 153 | $type | The type of element to create. (`div`, `form`, `textarea`, etc.) 154 | $components | Array of nested child nodes 155 | $text | Text content inside the element (for simple nodes with no $components) 156 | $html | Unescaped html content inside the element 157 | $init | A function that auto-executes when the element gets created 158 | $update | A function that auto-executes when any data stored inside the element changes 159 | 160 | 161 | For example, 162 | 163 | ```js 164 | var el = { 165 | $cell: true, 166 | $type: "div", 167 | $components: [ 168 | { $type: "span", $text: "Javascript" }, 169 | { $type: "span", $text: "objective-c" }, 170 | { $type: "span", $text: "ruby" }, 171 | { $type: "span", $text: "java" }, 172 | { $type: "span", $text: "lisp" } 173 | ] 174 | } 175 | ``` 176 | 177 | becomes: 178 | 179 | ```html 180 |
181 | Javascript 182 | objective-c 183 | ruby 184 | java 185 | lisp 186 |
187 | ``` 188 | 189 |
190 | 191 | ## Rule #3. Use the "_" Prefix to Store Data and Logic on an HTML Element 192 | 193 | Cell lets you store data and application logic directly on HTML elements. 194 | 195 | To define a variable on an element's context, simply prepend your attribute name with "_". Cell will treat it as data and make sure it doesn't affect the view. 196 | 197 | 198 | ```js 199 | el = { 200 | $cell: true, 201 | $type: "button", 202 | type: "button", 203 | $text: "Get next item", 204 | onclick: function(e) { this._next() }, 205 | _next: function() { 206 | this._index++; 207 | this.$text = this._items[this._index]; 208 | }, 209 | _index: 0, 210 | _items: ["javascript", "objective-c", "ruby", "java", "lisp"] 211 | } 212 | ``` 213 | 214 | Here we use `_items` to store an array, `_index` to store an integer counter, and `_next` to store a function that will run this element by incrementing `_index` and iterating through `_items`. 215 | 216 | 217 |
218 | 219 | 220 | 221 | 222 | # How it works 223 | 224 | ## 1. Cell is a Single Function that Creates a DOM Tree. 225 | 226 | When Cell loads, it first looks for all Javascript variables that have a `$cell` key. 227 | 228 | When it finds one, it takes that blueprint object (called a `"Genotype"` in Cell) and creates a DOM tree (`"Phenotype"`) from it. 229 | 230 |
231 | 232 | ![generator](https://s3-us-west-2.amazonaws.com/fm.ethan.jason/function.jpg) 233 | 234 | 235 |
236 | 237 | ## 2. Self-driving DOM 238 | 239 | So far this is just a static DOM tree. To make it dynamic, you need to write a program that "remote controls" these HTML elements. 240 | 241 | Normally Javascript frameworks maintain a separate **centralized data structure and application context ([Model-View-Controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) or some variation)** that synchronizes with and controls HTML elements dynamically. 242 | 243 | **Cell takes a decentralized approach.** It creates a DOM tree where each element is self-aware (It can contain an entire Model-View-Controller environment of its own) and can therefore "drive" itself autonomously (Internally called `"Nucleus"`). 244 | 245 |
246 | 247 | ![Image](https://s3-us-west-2.amazonaws.com/fm.ethan.jason/domtree.jpg) 248 | 249 |
250 | 251 | Instead of having a central master application control the DOM, **Cell directly injects application context into each relevant HTML element so they can run on their own, independent from the outside world.** 252 | 253 | Learn more about the underlying architecture [here](./GENESIS.md). 254 | 255 |
256 | 257 | 258 | 259 | # What problems this solves 260 | 261 | 262 | ## 1. There is No God (There is No Framework) 263 | 264 | Cell has no overarching framework that powers each and every corner of your app. 265 | 266 | ![Image](https://s3-us-west-2.amazonaws.com/fm.ethan.jason/architecture.jpg) 267 | 268 | Normally web app frameworks maintain a central "Model-View-Controller" architecture (or similar) which takes care of everything throughout the app's lifecycle. 269 | 270 | Cell works differently. It just creates the DOM and then goes away, because each HTML element it creates can self-drive itself with its **own** model-view-controller. Instead of controlling the DOM remotely with a framework's API, with Cell you control it directly and natively. 271 | 272 | 273 | Comparison |Frameworks before Cell | Cell 274 | -------------|--------------|------------------------------ 275 | Control | Centralized | Decentralized 276 | Structure | A master Model-View-Controller program that controls all the HTML elements | Each html element as the container of its own Model-View-Controller logic 277 | Your App | Full of framework API syntax | Just a vanilla Javascript. No framework code. 278 | Job | Manages everything throughout the entire app lifecycle | Runs exactly once at the beginning to create an autonomous DOM tree, and then goes away. 279 | 280 |
281 | 282 | ## 2. There are No Middlemen 283 | 284 | Nowadays, just to make a simple web app you need to learn all kinds of middlemen technologies. 285 | 286 | These tools were born out of necessity as web apps became more complex. But if you take a fundamentally different approach, you may not need them at all. 287 | 288 | ![Image](https://s3-us-west-2.amazonaws.com/fm.ethan.jason/process.jpg) 289 | 290 | Here are some of the reasons why these middlemen have been necessary, and **why Cell doesn't need them**. 291 | 292 | ##### 1. Frameworks have a class you have to inherit or extend. 293 | >Normally web app frameworks let you use their API by extending or inheriting from their class. Cell has no class and no API method. 294 | 295 | ##### 2. Frameworks depend on other libraries. 296 | > Most web app frameworks depend on other complex libraries (Don't forget to `npm install` before doing anything!) Cell doesn't depend on any library. 297 | 298 | ##### 3. Frameworks introduce dependencies. 299 | > Just by choosing to use a framework you have already lost the war against dependency. From then on, you need to use `npm install` for every frontend Javascript library you need to use. Cell frees you from this loop and lets you use frontend Javascript libraries with simple ` 3 | 4 | 5 | 6 | 28 | 29 | 30 | 31 |

32 |
33 | GitHub 34 | Demo 35 | Tutorial 36 | Twitter 37 | Slack 38 | 39 |

40 | 41 | Build Status 42 | Coverage Status 43 | 44 |
45 | 46 |
47 |
48 |
49 |

Subscribe to Newsletter

50 |

Stay updated on milestones, new projects, and useful tips from the community

51 |
52 | 53 |
54 |
55 | 56 | 57 |
58 | 59 |
60 | 61 |
62 |
63 |
64 |
65 | 66 |
67 | 68 | 69 |

Cell

70 |

A self-constructing web app framework powered by a self-driving DOM.

71 |
    72 |
  1. Philosophy
  2. 73 |
  3. Try Now
  4. 74 |
  5. How is it different?
  6. 75 |
  7. Rules
  8. 76 |
  9. How does it work?
  10. 77 |
  11. What problems does it solve?
  12. 78 |
79 |


80 |

Philosophy

81 |

Cell has one and only one design goal: Easy.

82 |
    83 |
  1. Easy to learn: There is NO API to learn. You just need to remember 3 rules.
  2. 84 |
  3. Easy to use: You just need a single HTML file with a single <script src> line.
  4. 85 |
  5. Easy to read: Write an entire app as a piece of JSON-like, human-readable data structure.
  6. 86 |
  7. Easy to integrate: Integrating into an existing website is as simple as copy and pasting a Youtube embed code.
  8. 87 |
  9. Easy to reuse: Everything is powered by stateless functions instead of 88 | es and objects, making it extremely modular.
  10. 89 |
  11. Easy to maintain: "Development workflow" doesn't exist. No NPM, No Webpack, No Babel, just vanilla Javascript and 100% based on web standards.
  12. 90 |
91 |


92 |

Try Now

93 |

Try downloading to your local machine and open it in your browser.

94 |

Seriously, there is no additional code or dependency, no environment to set up. What you see is what you get.

95 |

Download and Try it!

96 |
<html>
 97 | <script src="https://www.celljs.org/cell.js"></script>
 98 | <script>
 99 | var el = {
100 |   $cell: true,
101 |   style: "font-family: Helvetica; font-size: 14px;",
102 |   $components: [
103 |     {
104 |       $type: "input",
105 |       type: "text",
106 |       placeholder: "Type something and press enter",
107 |       style: "width: 100%; outline:none; padding: 5px;",
108 |       $init: function(e) { this.focus() },
109 |       onkeyup: function(e) {
110 |         if (e.keyCode === 13) {
111 |           document.querySelector("#list")._add(this.value);
112 |           this.value = "";
113 |         }
114 |       }
115 |     },
116 |     {
117 |       $type: "ol",
118 |       id: "list",
119 |       _items: [],
120 |       $components: [],
121 |       _add: function(val) { this._items.push(val) },
122 |       $update: function() {
123 |         this.$components = this._items.map(function(item) {
124 |           return { $type: "li", $text: item }
125 |         })
126 |       }
127 |     }
128 |   ]
129 | }
130 | </script>
131 | </html>
132 | 
133 |


134 |

Here's the generated DOM tree, as viewed in Chrome inspector:

135 |

autonomous dom

136 |


137 |

There Is No Framework

138 |

A couple of things to note from the code:

139 |
    140 |
  1. There are no framework classes to inherit and extend from.
  2. 141 |
  3. There are no API method calls.
  4. 142 |
  5. There are no HTML body tags.
  6. 143 |
  7. All we have is a single JSON-like variable.
  8. 144 |
  9. The DOM just builds itself without you running any function.
  10. 145 |
146 |


147 |

There are only 3 rules

148 |

Cell has no API. 100% of your code will be vanilla Javascript, and there is no framework method or class to implement.

149 |

To use Cell, you simply define a variable that describes the DOM content and behavior.

150 |

When you follow the 3 rules below, Cell turns it into HTML.

151 |


152 |

Rule #1. Attributes map 1:1 to DOM attributes by default.

153 |

When you define a Javascript object, its attributes map 1:1 to DOM attributes. So,

154 |
var node = {
155 |   id: "container",
156 |   class: "red"
157 | }
158 | 
159 |

maps to:

160 |
<div id="container" class="red"></div>
161 | 
162 |


163 |

Rule #2. Use 7 special keywords to declare the cell structure

164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
KeyDescription
$cellRequired. Tells Cell to create a cell element using this object as a root
$typeThe type of element to create. (div, form, textarea, etc.)
$componentsArray of nested child nodes
$textText content inside the element (for simple nodes with no $components)
$htmlUnescaped html content inside the element
$initA function that auto-executes when the element gets created
$updateA function that auto-executes when any data stored inside the element changes
202 |

For example,

203 |
var el = {
204 |   $cell: true,
205 |   $type: "div",
206 |   $components: [
207 |     { $type: "span", $text: "Javascript" },
208 |     { $type: "span", $text: "objective-c" },
209 |     { $type: "span", $text: "ruby" },
210 |     { $type: "span", $text: "java" },
211 |     { $type: "span", $text: "lisp" }
212 |   ]
213 | }
214 | 
215 |

becomes:

216 |
<div>
217 |   <span>Javascript</span>
218 |   <span>objective-c</span>
219 |   <span>ruby</span>
220 |   <span>java</span>
221 |   <span>lisp</span>
222 | </div>
223 | 
224 |


225 |

Rule #3. Use the "_" Prefix to Store Data and Logic on an HTML Element

226 |

Cell lets you store data and application logic directly on HTML elements.

227 |

To define a variable on an element's context, simply prepend your attribute name with "_". Cell will treat it as data and make sure it doesn't affect the view.

228 |
el = {
229 |   $cell: true,
230 |   $type: "button",
231 |   type: "button",
232 |   $text: "Get next item",
233 |   onclick: function(e) { this._next() },
234 |   _next: function() {
235 |     this._index++;
236 |     this.$text = this._items[this._index];
237 |   },
238 |   _index: 0,
239 |   _items: ["javascript", "objective-c", "ruby", "java", "lisp"]
240 | }
241 | 
242 |

Here we use _items to store an array, _index to store an integer counter, and _next to store a function that will run this element by incrementing _index and iterating through _items.

243 |


244 |

How it works

245 |

1. Cell is a Single Function that Creates a DOM Tree.

246 |

When Cell loads, it first looks for all Javascript variables that have a $cell key.

247 |

When it finds one, it takes that blueprint object (called a "Genotype" in Cell) and creates a DOM tree ("Phenotype") from it.

248 |


249 |

generator

250 |


251 |

2. Self-driving DOM

252 |

So far this is just a static DOM tree. To make it dynamic, you need to write a program that "remote controls" these HTML elements.

253 |

Normally Javascript frameworks maintain a separate centralized data structure and application context (Model-View-Controller or some variation) that synchronizes with and controls HTML elements dynamically.

254 |

Cell takes a decentralized approach. It creates a DOM tree where each element is self-aware (It can contain an entire Model-View-Controller environment of its own) and can therefore "drive" itself autonomously (Internally called "Nucleus").

255 |


256 |

Image

257 |


258 |

Instead of having a central master application control the DOM, Cell directly injects application context into each relevant HTML element so they can run on their own, independent from the outside world.

259 |

Learn more about the underlying architecture here.

260 |


261 |

What problems this solves

262 |

1. There is No God (There is No Framework)

263 |

Cell has no overarching framework that powers each and every corner of your app.

264 |

Image

265 |

Normally web app frameworks maintain a central "Model-View-Controller" architecture (or similar) which takes care of everything throughout the app's lifecycle.

266 |

Cell works differently. It just creates the DOM and then goes away, because each HTML element it creates can self-drive itself with its own model-view-controller. Instead of controlling the DOM remotely with a framework's API, with Cell you control it directly and natively.

267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 |
ComparisonFrameworks before CellCell
ControlCentralizedDecentralized
StructureA master Model-View-Controller program that controls all the HTML elementsEach html element as the container of its own Model-View-Controller logic
Your AppFull of framework API syntaxJust a vanilla Javascript. No framework code.
JobManages everything throughout the entire app lifecycleRuns exactly once at the beginning to create an autonomous DOM tree, and then goes away.
298 |


299 |

2. There are No Middlemen

300 |

Nowadays, just to make a simple web app you need to learn all kinds of middlemen technologies.

301 |

These tools were born out of necessity as web apps became more complex. But if you take a fundamentally different approach, you may not need them at all.

302 |

Image

303 |

Here are some of the reasons why these middlemen have been necessary, and why Cell doesn't need them.

304 |
1. Frameworks have a class you have to inherit or extend.
305 |
306 |

Normally web app frameworks let you use their API by extending or inheriting from their class. Cell has no class and no API method.

307 |
308 |
2. Frameworks depend on other libraries.
309 |
310 |

Most web app frameworks depend on other complex libraries (Don't forget to npm install before doing anything!) Cell doesn't depend on any library.

311 |
312 |
3. Frameworks introduce dependencies.
313 |
314 |

Just by choosing to use a framework you have already lost the war against dependency. From then on, you need to use npm install for every frontend Javascript library you need to use. Cell frees you from this loop and lets you use frontend Javascript libraries with simple <script src>.

315 |
316 |
4. Framework-specific markup needs to be compiled.
317 |
318 |

Cell stays away from inventing any framework-specific markup such as HTML templates. There's no template to compile.

319 |
320 |
5. Frameworks require you to transpile, compile, and/or build your app to make it work.
321 |
322 |

Cell is built with ES5, which works in ALL browsers (including IE). There's no need to transpile your code to use Cell, it just works right away.

323 |
324 |


325 |

3. App as Data

326 |

Cell is based on the same idea behind Jasonette, a simple way to build cross-platform iOS/Android native apps with nothing but JSON markup.

327 |

Just like Jasonette, Cell lets you express application logic as a piece of flat data. This allows you to not only transform and manipulate data, but also the application logic itself.

328 |

Let's say we have this view:

329 |
var El = {
330 |   $cell: true,
331 |   class: "container",
332 |   $components: [
333 |     { $type: "span", $text: "Four Barrel", class: "row" },
334 |     { $type: "span", $text: "Philz", class: "row" },
335 |     { $type: "span", $text: "Blue Bottle", class: "row" },
336 |     { $type: "span", $text: "Stumptown", class: "row" },
337 |     { $type: "span", $text: "Counter Culture", class: "row" }
338 |   ]
339 | }
340 | 
341 |

We see many repeating span lines, so let's extract them out into a function:

342 |
Coffee = ["Four Barrel", "Philz", "Blue Bottle", "Stumptown", "Counter Culture"]
343 | Item = function(brand) {
344 |   return { $type: "span", $text: brand, class: "row" }
345 | }
346 | var El = {
347 |   $cell: true,
348 |   class: "container",
349 |   $components: Coffee.map(Item)
350 | }
351 | 
352 |

Notice how the Item is simply a stateless function. We run a map on it with the Coffee array and end up with the same structure as before.

353 |


354 |

4. Extreme Modularity with Functional Programming

355 |

Normally web app frameworks implement reusable components with classes. You need to extend the framework's class and then create components from its instance.

356 |

A "component" on Cell is nothing more than a stateless function. This is extremely liberating because functions have zero overhead compared to classes.

357 |

Because of this functional programming approach:

358 |
    359 |
  1. You can split out your app into as many modules as you want.
  2. 360 |
  3. Being able to break down your app into such granular pieces makes it extremely reusable, even in other apps.
  4. 361 |
  5. "Components" are not just for views anymore. Because your app logic fits into a JSON-like object that can be easily transformed, filtered, and manipulated, components can encapsulate the entire Model-View-Controller.
  6. 362 |
363 |


364 |

5. Write Future-proof Code

365 |

Normally when you use a web app framework, you write code that heavily depends on the framework API.

366 |

So if you ever want to use a new framework, you have to rewrite the entire app, taking a huge amount of time to make it do exactly the same thing it used to do.

367 |

With Cell, your can write code that never becomes useless, simply because:

368 |
    369 |
  1. Cell doesn't have an API, so there's nothing to "depend" on.
  2. 370 |
  3. With the functional programming approach, you can write infinitely modular code.
  4. 371 |
372 |


373 |

6. Native DOM as App Container

374 |

Being able to containerize your app's logic and data inside its HTML elements and then "ship" it to the DOM enables a lot of cool things.

375 |

container

376 |

A. Integrate with ANY Web Technology Natively.

377 |
378 |

Looks like a DOM, Acts like a DOM, it actually IS a DOM

379 |
380 |

Cell creates an "Actual DOM". There's nothing virtual or magical about it. It really IS just a pure HTML element.

381 |

This means we can apply any 3rd party Javascript or CSS libraries (like jQuery, Bootstrap, Foundation, Select2, CodeMirror, etc.) the same way we would use it on vanilla HTML.

382 |


383 |

B. Plug into EXISTING Websites like a widget.

384 |

Normally, using a web app framework is an all or nothing deal, because the framework takes over your entire frontend.

385 |

Cell completely encapsulates your app's logic into discrete HTML elements, so integrating it into an existing web app or website is as simple as copy and paste.

386 |
387 | 388 |


389 |
390 | 391 |


392 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@intercellular/cell", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "cell.js", 6 | "scripts": { 7 | "uglify": "uglifyjs cell.js --compress --mangle --output cell.min.js", 8 | "test:lint": "eslint cell.js", 9 | "test:mocha": "nyc --reporter=html --reporter=text _mocha --require jsdom-global/register", 10 | "test": "npm run test:lint && npm run test:mocha", 11 | "coverage": "nyc report --reporter=text-lcov | coveralls", 12 | "site": "node ./node_modules/phd" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "coveralls": "^2.13.1", 18 | "eslint": "^4.0.0", 19 | "jsdom": "^11.0.0", 20 | "jsdom-global": "^3.0.2", 21 | "json-stable-stringify": "^1.0.1", 22 | "mocha": "^3.3.0", 23 | "nyc": "^11.0.3", 24 | "phd": "^1.0.4", 25 | "sinon": "^2.2.0", 26 | "uglify-js": "^3.0.15" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /phd.config.js: -------------------------------------------------------------------------------- 1 | module.exports = [{ 2 | js: [ 3 | 'https://www.celljs.org/cell.js', 4 | 'https://cdnjs.cloudflare.com/ajax/libs/timeago.js/3.0.1/timeago.min.js', 5 | 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.11.0/highlight.min.js', 6 | './website/demos/twitter.js', 7 | './website/demos/bitcoin.js' 8 | ], 9 | css: [ 10 | 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.11.0/styles/grayscale.min.css', 11 | 'https://fonts.googleapis.com/css?family=Raleway:300,400,500,600,700', 12 | 'https://fonts.googleapis.com/css?family=Merriweather:900', 13 | './website/style.css' 14 | ], 15 | inject: { 16 | "#mailchimp": "./website/components/mailchimp/form.html" 17 | }, 18 | init: function() { 19 | 20 | var container = document.createElement('div'); 21 | container.className = 'container'; 22 | document.body.appendChild(container); 23 | 24 | var phd = document.getElementById('phd'); 25 | container.appendChild(phd); 26 | 27 | document.querySelector("#widget").$build($root); 28 | document.querySelector("#twitter").$build(T); 29 | hljs.initHighlighting(); 30 | 31 | (function(i,s,o,g,r,a,m) {i['GoogleAnalyticsObject']=r;i[r]=i[r]||function() { 32 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 33 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 34 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); 35 | ga('create', 'UA-54282166-10', 'auto'); 36 | ga('send', 'pageview'); 37 | } 38 | }] 39 | -------------------------------------------------------------------------------- /test/Gene.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const stringify = require('json-stable-stringify') 3 | const {Gene} = require("../cell") 4 | const compare = function(actual, expected) { 5 | assert.equal(stringify(actual), stringify(expected)); 6 | } 7 | describe("Gene", function() { 8 | describe("insertion only", function() { 9 | it("prepend", function() { 10 | var _old = ["b", "c", "d", "e"] 11 | var _new = ["a", "b", "c", "d", "e"] 12 | var LCS = Gene.LCS(_old, _new) 13 | var Diff = Gene.diff(_old, _new) 14 | compare(LCS, [ 15 | { item: 'b', _old: 0, _new: 1 }, 16 | { item: 'c', _old: 1, _new: 2 }, 17 | { item: 'd', _old: 2, _new: 3 }, 18 | { item: 'e', _old: 3, _new: 4 } 19 | ]) 20 | compare(Diff, { '-': [], '+': [ { item: 'a', index: 0 } ] }) 21 | }) 22 | it("append", function() { 23 | var _old = ["b", "c", "d", "e"] 24 | var _new = ["b", "c", "d", "e", "f"] 25 | var LCS = Gene.LCS(_old, _new) 26 | var Diff = Gene.diff(_old, _new) 27 | compare(LCS, [ 28 | { item: 'b', _old: 0, _new: 0 }, 29 | { item: 'c', _old: 1, _new: 1 }, 30 | { item: 'd', _old: 2, _new: 2 }, 31 | { item: 'e', _old: 3, _new: 3 } 32 | ]) 33 | compare(Diff, { '-': [], '+': [ { item: 'f', index: 4 } ] }) 34 | }) 35 | it("middle", function() { 36 | var _old = ["a", "b", "d", "e"] 37 | var _new = ["a", "b", "c", "d", "e"] 38 | var LCS = Gene.LCS(_old, _new) 39 | var Diff = Gene.diff(_old, _new) 40 | compare(LCS, [ 41 | { item: 'a', _old: 0, _new: 0 }, 42 | { item: 'b', _old: 1, _new: 1 }, 43 | { item: 'd', _old: 2, _new: 3 }, 44 | { item: 'e', _old: 3, _new: 4 } 45 | ]) 46 | compare(Diff, { '-': [], '+': [ { item: 'c', index: 2 } ] }) 47 | }) 48 | }) 49 | describe("deletion only", function() { 50 | it("from end", function() { 51 | var _old = ["a", "b", "d", "e"] 52 | var _new = ["a", "b", "d"] 53 | var LCS = Gene.LCS(_old, _new) 54 | var Diff = Gene.diff(_old, _new) 55 | compare(LCS, [ 56 | { item: 'a', _old: 0, _new: 0 }, 57 | { item: 'b', _old: 1, _new: 1 }, 58 | { item: 'd', _old: 2, _new: 2 } 59 | ]) 60 | compare(Diff, { '-': [ {item: 'e', index: 3} ], '+': [] }) 61 | }) 62 | it("from start", function() { 63 | var _old = ["a", "b", "d", "e"] 64 | var _new = ["b", "d", "e"] 65 | var LCS = Gene.LCS(_old, _new) 66 | var Diff = Gene.diff(_old, _new) 67 | compare(LCS, [ 68 | { item: 'b', _old: 1, _new: 0 }, 69 | { item: 'd', _old: 2, _new: 1 }, 70 | { item: 'e', _old: 3, _new: 2 } 71 | ]) 72 | compare(Diff, { '-': [ {item: 'a', index: 0} ], '+': [] }) 73 | }) 74 | it("from middle", function() { 75 | var _old = ["a", "b", "d", "e"] 76 | var _new = ["a", "e"] 77 | var LCS = Gene.LCS(_old, _new) 78 | var Diff = Gene.diff(_old, _new) 79 | compare(LCS, [ 80 | { item: 'a', _old: 0, _new: 0 }, 81 | { item: 'e', _old: 3, _new: 1 } 82 | ]) 83 | compare(Diff, { '-': [ {item: 'b', index: 1}, {item: 'd', index: 2} ], '+': [] }) 84 | }) 85 | it("multiple from middle", function() { 86 | var _old = ["a", "b", "c", "d", "e", "f"] 87 | var _new = ["a", "c", "d", "f"] 88 | var LCS = Gene.LCS(_old, _new) 89 | var Diff = Gene.diff(_old, _new) 90 | compare(LCS, [ 91 | { item: 'a', _old: 0, _new: 0 }, 92 | { item: 'c', _old: 2, _new: 1 }, 93 | { item: 'd', _old: 3, _new: 2 }, 94 | { item: 'f', _old: 5, _new: 3 } 95 | ]) 96 | compare(Diff, { '-': [ {item: 'b', index: 1}, {item: 'e', index: 4} ], '+': [] }) 97 | }) 98 | }) 99 | describe("insertion and deletion", function() { 100 | it("from middle", function() { 101 | var _old = ["a", "b", "d", "e"] 102 | var _new = ["a", "c", "e"] 103 | var LCS = Gene.LCS(_old, _new) 104 | var Diff = Gene.diff(_old, _new) 105 | compare(LCS, [ 106 | { item: 'a', _old: 0, _new: 0 }, 107 | { item: 'e', _old: 3, _new: 2 } 108 | ]) 109 | compare(Diff, { '-': [ {item: 'b', index: 1}, {item: 'd', index: 2} ], '+': [ {item: 'c', index: 1} ] }) 110 | }) 111 | it("multiple from middle", function() { 112 | var _old = ["a", "b", "c", "d", "e", "f", "g"] 113 | var _new = ["a", "c", "d", "h", "g"] 114 | var LCS = Gene.LCS(_old, _new) 115 | var Diff = Gene.diff(_old, _new) 116 | compare(LCS, [ 117 | { item: 'a', _old: 0, _new: 0 }, 118 | { item: 'c', _old: 2, _new: 1 }, 119 | { item: 'd', _old: 3, _new: 2 }, 120 | { item: 'g', _old: 6, _new: 4 } 121 | ]) 122 | compare(Diff, { '-': [ {item: 'b', index: 1}, {item: 'e', index: 4}, {item: 'f', index: 5} ], '+': [ {item: 'h', index: 3} ] }) 123 | }) 124 | }) 125 | describe("complex", function() { 126 | it("array", function() { 127 | var _old = [ 128 | { 129 | "$type":"li","class":"","_model":{"todo":"a","completed":false}, 130 | "$components":[ 131 | { 132 | "class":"view", 133 | "$components":[ 134 | {"$type":"input","class":"toggle","type":"checkbox"}, 135 | {"$type":"label","$text":"a"}, 136 | {"$type":"button","class":"destroy"}, 137 | {"$type":"input","class":"edit","value":"a"} 138 | ] 139 | } 140 | ] 141 | } 142 | ]; 143 | var _new = [ 144 | { 145 | "$type":"li","class":"","_model":{"todo":"a","completed":false}, 146 | "$components":[ 147 | { 148 | "class":"view", 149 | "$components":[ 150 | {"$type":"input","class":"toggle","type":"checkbox"}, 151 | {"$type":"label","$text":"a"}, 152 | {"$type":"button","class":"destroy"}, 153 | {"$type":"input","class":"edit","value":"a"} 154 | ] 155 | } 156 | ] 157 | }, 158 | { 159 | "$type":"li", 160 | "class":"", 161 | "_model":{"todo":"b","completed":false}, 162 | "$components":[ 163 | { 164 | "class":"view", 165 | "$components":[ 166 | {"$type":"input","class":"toggle","type":"checkbox"}, 167 | {"$type":"label","$text":"b"}, 168 | {"$type":"button","class":"destroy"}, 169 | {"$type":"input","class":"edit","value":"b"} 170 | ] 171 | } 172 | ] 173 | } 174 | ]; 175 | var LCS = Gene.LCS(_old, _new); 176 | var Diff = Gene.diff(_old, _new); 177 | compare(LCS, [ { item: _old[0], _old: 0, _new: 0 } ]) 178 | compare(Diff, { '-': [ ], "+": [ {item: _new[1], index: 1} ] }); 179 | }) 180 | it("json", function() { 181 | var _old = [ 182 | { 183 | "$type": "li", 184 | "class": "", 185 | "_model": { 186 | "todo": "a", 187 | "completed": false 188 | }, 189 | "$components": [ 190 | { 191 | "class": "view", 192 | "$components": [ 193 | { 194 | "$type": "input", 195 | "class": "toggle", 196 | "type": "checkbox" 197 | }, 198 | { 199 | "$type": "label", 200 | "$text": "a" 201 | }, 202 | { 203 | "$type": "button", 204 | "class": "destroy" 205 | }, 206 | { 207 | "$type": "input", 208 | "class": "edit", 209 | "value": "a" 210 | } 211 | ] 212 | } 213 | ] 214 | } 215 | ]; 216 | 217 | var _new = [ 218 | { 219 | "$type": "li", 220 | "class": "", 221 | "_model": { 222 | "todo": "b", 223 | "completed": false 224 | }, 225 | "$components": [ 226 | { 227 | "class": "view", 228 | "$components": [ 229 | { 230 | "$type": "input", 231 | "class": "toggle", 232 | "type": "checkbox" 233 | }, 234 | { 235 | "$type": "label", 236 | "$text": "b" 237 | }, 238 | { 239 | "$type": "button", 240 | "class": "destroy" 241 | }, 242 | { 243 | "$type": "input", 244 | "class": "edit", 245 | "value": "b" 246 | } 247 | ] 248 | } 249 | ] 250 | }, 251 | { 252 | "$type": "li", 253 | "class": "", 254 | "_model": { 255 | "todo": "a", 256 | "completed": false 257 | }, 258 | "$components": [ 259 | { 260 | "class": "view", 261 | "$components": [ 262 | { 263 | "$type": "input", 264 | "class": "toggle", 265 | "type": "checkbox" 266 | }, 267 | { 268 | "$type": "label", 269 | "$text": "a" 270 | }, 271 | { 272 | "$type": "button", 273 | "class": "destroy" 274 | }, 275 | { 276 | "$type": "input", 277 | "class": "edit", 278 | "value": "a" 279 | } 280 | ] 281 | } 282 | ] 283 | } 284 | ] 285 | 286 | var LCS = Gene.LCS(_old, _new) 287 | var Diff = Gene.diff(_old, _new); 288 | compare(LCS, [{item: _old[0], _old: 0, _new: 1}]); 289 | compare(Diff, { '-': [], '+': [ { item: _new[0], index: 0 } ] }); 290 | 291 | }) 292 | }) 293 | }) 294 | -------------------------------------------------------------------------------- /test/Genotype.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const sinon = require('sinon') 3 | const stringify = require('json-stable-stringify') 4 | const {God, Genotype, Nucleus} = require("../cell") 5 | const spy = require("./spy.js") 6 | const compare = function(actual, expected) { 7 | assert.equal(stringify(actual), stringify(expected)); 8 | } 9 | describe("Genotype", function() { 10 | require('jsdom-global')() 11 | God.plan(window); 12 | describe("indenpendent from set", function() { 13 | describe("update", function() { 14 | it("calls Genotype.set and Nuclues queue", function() { 15 | const $node = document.createElement("div") 16 | $node.Genotype = {} 17 | $node.Meta = {} 18 | Nucleus._queue = [] 19 | 20 | spy.Genotype.set.reset(); 21 | Genotype.update($node, "class", "red") 22 | 23 | // After 24 | // Nucleus.queue.length > 0 25 | compare(Nucleus._queue.length, 1) 26 | // Genotype.set called 27 | compare(spy.Genotype.set.callCount, 1) 28 | }) 29 | }) 30 | describe("build", function() { 31 | it("if $node.Meta.prokaryotic, do nothing", function() { 32 | const $node = document.createElement("div") 33 | $node.Meta = { 34 | prokaryotic: true 35 | } 36 | spy.Genotype.set.reset() 37 | 38 | Genotype.build($node, {}, null) 39 | compare(spy.Genotype.set.callCount, 0) 40 | }) 41 | it("Genotype.set called multiple times for each key", function() { 42 | const $node = document.createElement("div") 43 | $node.Genotype = {} 44 | $node.Meta = {} 45 | 46 | spy.Genotype.set.reset() 47 | 48 | Genotype.build($node, {$type: "div", class: "red", fun: function() { }}) 49 | compare(spy.Genotype.set.callCount, 3) 50 | }) 51 | }) 52 | describe("set", function() { 53 | describe("Function binding (Nucleus.bind)", function() { 54 | it("$init doesn't run Nucleus.bind", function() { 55 | // Nucleus.bind makes sure that $update() is run after the function is executed. 56 | // In case of $init() we don't want this behavior because $init() will take care of it 57 | // on its own 58 | const $node = document.createElement("div") 59 | $node.Genotype = {} 60 | $node.Meta = {} 61 | 62 | spy.Nucleus.bind.reset() 63 | 64 | Genotype.set($node, "$init", function() { 65 | // blah blah 66 | }) 67 | compare(spy.Nucleus.bind.callCount, 0); 68 | }) 69 | it("other functions run Nucleus.bind", function() { 70 | const $node = document.createElement("div") 71 | $node.Genotype = {} 72 | $node.Meta = {} 73 | 74 | spy.Nucleus.bind.reset() 75 | 76 | Genotype.set($node, "_fun", function() { 77 | // blah blah 78 | }) 79 | compare(spy.Nucleus.bind.callCount, 1); 80 | }) 81 | }) 82 | describe("non collection", function() { 83 | it("calls Nucleus.bind", function() { 84 | const $node = document.createElement("div") 85 | $node.Genotype = {} 86 | $node.Meta = {} 87 | 88 | spy.Nucleus.bind.reset() 89 | 90 | Genotype.set($node, "$type", "div") 91 | 92 | compare(spy.Nucleus.bind.callCount, 1) 93 | }) 94 | }) 95 | }) 96 | }) 97 | describe("AFTER set", function() { 98 | var $node; 99 | beforeEach(function() { 100 | $node = document.createElement("div") 101 | $node.Genotype = {} 102 | $node.Meta = {} 103 | spy.Nucleus.bind.reset() 104 | spy.Nucleus.queue.reset() 105 | Genotype.set($node, "$components", [{ 106 | class: "red" 107 | }, { 108 | class: "green" 109 | }, { 110 | class: "blue" 111 | }]) 112 | Genotype.set($node, "keys", { 113 | a: "A", 114 | b: "B", 115 | c: "C" 116 | }) 117 | }) 118 | afterEach(function() { 119 | spy.Nucleus.bind.reset() 120 | spy.Nucleus.queue.reset() 121 | Nucleus._queue = [] 122 | }) 123 | it("setting a diffrent attribute on the node has nothing to do with this", function() { 124 | Genotype.set($node, "$type", "p") 125 | compare(spy.Nucleus.queue.callCount, 0) 126 | }) 127 | it("if the value is the same, don't call Nucleus.queue (object)", function() { 128 | $node.Genotype.$components[0] = {class: "red"} 129 | compare(spy.Nucleus.queue.callCount, 0) 130 | }) 131 | it("if the value is the same, don't call Nucleus.queue (array)", function() { 132 | $node.Genotype.keys.b = "B"; 133 | compare(spy.Nucleus.queue.callCount, 0) 134 | compare($node.Genotype.keys, { 135 | a: "A", 136 | b: "B", 137 | c: "C" 138 | }) 139 | }) 140 | describe("$virus", function() { 141 | let ul_mutating_virus = function(component){ 142 | component.id = "infected"; 143 | component.$components = [{$type: 'li'}]; 144 | return component; 145 | } 146 | it("Applies a single virus mutation", function() { 147 | let component = { $type: 'ul', $virus: ul_mutating_virus }; 148 | 149 | compare(Genotype.infect(component), { 150 | $type: 'ul', 151 | $components: [{$type: 'li'}], 152 | id: 'infected', 153 | }) 154 | 155 | let $node = root.document.body.$build(component, []); 156 | compare($node.outerHTML, ''); 157 | }) 158 | it("Applies multiple virus mutations sequentially", function() { 159 | let id_mutating_virus = function(component){ 160 | component._previous_id = component.id; 161 | component.id += "_again"; 162 | return component; 163 | } 164 | 165 | let component = { 166 | $type: 'ul', 167 | $virus: [ul_mutating_virus, id_mutating_virus] 168 | }; 169 | 170 | compare(Genotype.infect(component), { 171 | $type: 'ul', 172 | $components: [{$type: 'li'}], 173 | _previous_id: 'infected', 174 | id: 'infected_again', 175 | }) 176 | 177 | let $node = root.document.body.$build(component, []); 178 | compare($node.outerHTML, ''); 179 | }) 180 | it("Errors when mutation does not comply with the API", function() { 181 | let wrong_mutation = function(component){ 182 | let the_thing = "return nothing"; 183 | } 184 | 185 | let component = { $type: 'ul', $virus: wrong_mutation }; 186 | assert.throws(() => Genotype.infect(component), /return an object/) 187 | }) 188 | }) 189 | }) 190 | }) 191 | -------------------------------------------------------------------------------- /test/God.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const sinon = require('sinon') 3 | const spy = require("./spy.js") 4 | const stringify = require('json-stable-stringify') 5 | const {God} = require("../cell") 6 | const compare = function(actual, expected) { 7 | assert.equal(stringify(actual), stringify(expected)); 8 | } 9 | // Everything that interfaces with the outside world 10 | describe("God", function() { 11 | require('jsdom-global')() 12 | God.plan(window); 13 | describe("God.detect(): finding $cell", function() { 14 | describe("looks for a variable with the key '$cell'", function() { 15 | it("only cells", function() { 16 | var mock = { 17 | gene1: { 18 | $type: "div", 19 | $text: "node1", 20 | $cell: true 21 | }, 22 | gene2: { 23 | $type: "span", 24 | $text: "node2", 25 | $cell: true 26 | } 27 | }; 28 | var genes = God.detect(mock); 29 | compare(genes.length, 2) 30 | compare(genes, [mock.gene1, mock.gene2]); 31 | }) 32 | it("mix of cells and non-cells", function() { 33 | var mock = { 34 | gene: { 35 | $type: "div", 36 | $text: "node1", 37 | $cell: true 38 | }, 39 | obj: { 40 | blah: "blah" 41 | }, 42 | str: "str", 43 | num: 1 44 | }; 45 | var genes = God.detect(mock); 46 | compare(genes.length, 1) 47 | compare(genes[0], mock.gene); 48 | }) 49 | }) 50 | }) 51 | describe("God.create()", function() { 52 | it("inserts into existing body correctly", function() { 53 | document.body.innerHTML = ""; 54 | window.gene = { 55 | $type: "body", 56 | $cell: true, 57 | $components: [{ 58 | $text: "Hello", 59 | class: "container" 60 | }] 61 | }; 62 | var $result = God.create(window); 63 | compare($result.map(function($node) { return $node.outerHTML }), [ '
Hello
' ]); 64 | compare(document.querySelector("html").outerHTML, '
Hello
'); 65 | }) 66 | it("attaches new node into body", function() { 67 | document.body.innerHTML = ""; 68 | window.gene = { 69 | $cell: true, 70 | $text: "Hello", 71 | class: "container" 72 | }; 73 | var $result = God.create(window); 74 | compare($result.map(function($node) { return $node.outerHTML }), [ '
Hello
' ]); 75 | compare(document.querySelector("html").outerHTML, '
Hello
'); 76 | }) 77 | describe("injects node into the id slot", function() { 78 | it("single id", function() { 79 | document.body.innerHTML = "
This is a row
"; 80 | window.gene = { 81 | id: "widget", 82 | $cell: true, 83 | $components: [{ 84 | $text: "Hello", 85 | class: "ticker" 86 | }] 87 | }; 88 | var $result = God.create(window); 89 | compare(document.body.innerHTML, "
This is a row
Hello
"); 90 | }) 91 | it("multiple ids", function() { 92 | document.body.innerHTML = "
This is a row
"; 93 | window.gene = { 94 | class: "sidebar", 95 | id: "widget", 96 | $cell: true, 97 | $components: [{ 98 | $text: "Hello", 99 | class: "ticker" 100 | }] 101 | }; 102 | window.gene2 = { 103 | id: "search", 104 | $cell: true, 105 | $type: "input", 106 | type: "search" 107 | }; 108 | var $result = God.create(window); 109 | compare(document.body.innerHTML, "
This is a row
Hello
"); 110 | }) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /test/Membrane.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const sinon = require('sinon') 3 | const spy = require("./spy.js") 4 | const stringify = require('json-stable-stringify') 5 | const {God, Membrane} = require("../cell") 6 | const compare = function(actual, expected) { 7 | assert.equal(stringify(actual), stringify(expected)); 8 | } 9 | describe("Membrane", function() { 10 | require('jsdom-global')() 11 | God.plan(window); 12 | describe("build", function() { 13 | describe("Membrane.inject", function() { 14 | it("get existing head", function() { 15 | spy.Membrane.inject.reset(); 16 | spy.Membrane.add.reset(); 17 | var gene = { 18 | $type: "head", 19 | $cell: true, 20 | $components: [{ 21 | $type: "link", 22 | href: "http://localhost", 23 | rel: "stylesheet" 24 | }] 25 | } 26 | var $node = Membrane.build(window, gene, null, null) 27 | assert.equal($node, document.head) 28 | compare(spy.Membrane.inject.callCount, 1) 29 | compare(spy.Membrane.add.callCount, 0) 30 | }) 31 | it("get existing body", function() { 32 | spy.Membrane.inject.reset(); 33 | spy.Membrane.add.reset(); 34 | var gene = { 35 | $type: "body", 36 | $cell: true, 37 | $components: [{ class: "container" }] 38 | } 39 | var $node = Membrane.build(window, gene, null, null) 40 | compare(Object.getPrototypeOf($node).toString(), "[object HTMLBodyElement]") 41 | compare($node.Meta, {}) 42 | compare($node, document.body); 43 | compare(spy.Membrane.inject.callCount, 1) 44 | compare(spy.Membrane.add.callCount, 0) 45 | }) 46 | it("get existing id", function() { 47 | spy.Membrane.inject.reset(); 48 | spy.Membrane.add.reset(); 49 | document.body.innerHTML = ""; 50 | var $widget = document.createElement("div"); 51 | $widget.setAttribute("id", "widget"); 52 | document.body.appendChild($widget); 53 | 54 | var gene = { 55 | id: "widget", 56 | $cell: true, 57 | $components: [{ class: "container" }] 58 | } 59 | var $node = Membrane.build(window, gene, null, null) 60 | 61 | compare(document.body.outerHTML, "
") 62 | compare($node, $widget); // same instance 63 | compare(spy.Membrane.inject.callCount, 1) 64 | compare(spy.Membrane.add.callCount, 0) 65 | }) 66 | }) 67 | describe("Membrane.add", function() { 68 | it("returns a newly created node", function() { 69 | spy.Membrane.inject.reset(); 70 | spy.Membrane.add.reset(); 71 | const $parent = document.createElement("div"); 72 | const $child = Membrane.build($parent, {}) 73 | compare($child.nodeType, 1) 74 | compare(spy.Membrane.inject.callCount, 1) 75 | compare(spy.Membrane.add.callCount, 1) 76 | }) 77 | it("the passed in node should be the parent of the returned node", function() { 78 | const $parent = document.createElement("div"); 79 | const $child = Membrane.build($parent, {}) 80 | assert.equal($child.parentNode, $parent) 81 | }) 82 | it("appends child", function() { 83 | spy.Phenotype.$type.reset(); 84 | const $node = document.createElement("div") 85 | Membrane.build($node, {$type: "span"}) 86 | compare(spy.Phenotype.$type.callCount, 1) 87 | compare($node.innerHTML, "") 88 | compare($node.outerHTML, "
") 89 | }) 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /test/Nucleus.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const sinon = require('sinon') 3 | const stringify = require('json-stable-stringify') 4 | const {God, Phenotype, Genotype, Nucleus} = require("../cell") 5 | const spy = require("./spy.js") 6 | const compare = function(actual, expected) { 7 | assert.equal(stringify(actual), stringify(expected)); 8 | } 9 | describe("Nucleus", function() { 10 | require('jsdom-global')() 11 | God.plan(window); 12 | describe("build", function() { 13 | // Builds a proxy 14 | describe("initial build", function() { 15 | it("creates a nucleus object", function() { 16 | const $node = document.createElement("div") 17 | spy.O.defineProperty.reset() 18 | $node.Meta = {} 19 | $node.Genotype = { 20 | $type: "div", 21 | $text: "Hi", 22 | $components: [] 23 | } 24 | Nucleus.build($node) 25 | compare(spy.O.defineProperty.callCount, 4) 26 | }) 27 | it("$text and $components are tracked by default", function() { 28 | const $node = document.createElement("div") 29 | spy.O.defineProperty.reset() 30 | $node.Meta = {} 31 | $node.Genotype = { 32 | $type: "div" 33 | } 34 | Nucleus.build($node) 35 | // $text and $components are tracked by default 36 | compare(spy.O.defineProperty.callCount, 4) 37 | }) 38 | }) 39 | describe("behaviors after the build", function() { 40 | describe("get", function() { 41 | let $div; 42 | beforeEach(function() { 43 | $div = document.createElement("div") 44 | $div.Meta = {} 45 | $div.Genotype = {} 46 | Nucleus.build($div) 47 | }) 48 | describe("$/_ variables", function() { 49 | it("properties that were manually assigned", function() { 50 | $div.Genotype = { 51 | $init: function() { 52 | //This function gets run automatically at init! 53 | }, 54 | $update: function() { 55 | this.$text = (this.done ? "done" : "todo") 56 | }, 57 | _done: false 58 | } 59 | Nucleus.build($div) 60 | Phenotype.build($div, $div.Genotype) 61 | spy.Nucleus.bind.reset(); 62 | spy.Phenotype.$init.reset(); 63 | spy.Phenotype.$update.reset(); 64 | $div.$init(); 65 | 66 | // $init doesn't call update 67 | compare(spy.Nucleus.bind.callCount, 0) 68 | compare(spy.Phenotype.$update.callCount, 0) 69 | 70 | }) 71 | }) 72 | describe("DOM attributes", function() { 73 | it("properties that were explicitly set by the user", function() { 74 | // test 1 75 | spy.Nucleus.bind.reset(); 76 | spy.Genotype.update.reset(); 77 | var c = $div.class; 78 | 79 | compare(spy.Genotype.update.callCount, 0); 80 | compare(c, undefined) 81 | 82 | $div.Genotype = { 83 | class: "red" 84 | } 85 | Nucleus.build($div) 86 | 87 | // test 2 88 | spy.Genotype.update.reset(); 89 | $div.class = "red"; 90 | 91 | spy.O.getOwnPropertyDescriptor.reset(); 92 | var d = $div.class; 93 | compare(spy.O.getOwnPropertyDescriptor.callCount, 0); 94 | }) 95 | it("properties that already exist on the DOM", function() { 96 | // For example, "tagName", "nodeType", etc. already exist on the element natively, and users nomrally don't set these manually. But sometimes we need to access these 97 | spy.Nucleus.bind.reset(); 98 | var name = $div.tagName; 99 | compare(spy.Nucleus.bind.callCount, 0); 100 | compare(name.toLowerCase(), "div") 101 | }) 102 | describe("special properties", function() { 103 | it("style", function() { 104 | spy.Nucleus.bind.reset(); 105 | spy.Genotype.update.reset(); 106 | var style = $div.style; 107 | 108 | compare(spy.Genotype.update.callCount, 0); 109 | 110 | // the getter returns a CSSStyleDeclaration object 111 | compare(Object.getPrototypeOf(style).constructor.name, "CSSStyleDeclaration"); 112 | 113 | // node setup 114 | $div.Genotype = { 115 | style: "background-color: red;" 116 | } 117 | Nucleus.build($div) 118 | 119 | spy.O.getOwnPropertyDescriptor.reset(); 120 | 121 | // getter is called, and the key is 'style' 122 | // so Object.getOwnPropertyDescriptor is called 123 | style = $div.style; 124 | compare(spy.O.getOwnPropertyDescriptor.callCount, 1); 125 | // the getter returns a CSSStyleDeclaration object 126 | compare(Object.getPrototypeOf(style).constructor.name, "CSSStyleDeclaration"); 127 | 128 | // string type style setter 129 | spy.Genotype.update.reset(); 130 | spy.O.getOwnPropertyDescriptor.reset(); 131 | $div.style = "background-color: blue;"; 132 | // since the setter is called and it's not an object type style, 133 | // it will trigger setAttribute and not Object.getOwnPropertyDescriptor 134 | compare(spy.O.getOwnPropertyDescriptor.callCount, 0); 135 | 136 | // object type style setter 137 | spy.O.getOwnPropertyDescriptor.reset(); 138 | $div.style = { 139 | backgroundColor: "blue" 140 | } 141 | // triggers setter, which triggers getter, both of which calls Object.getOwnPropertyDescriptor => 2 142 | compare(spy.O.getOwnPropertyDescriptor.callCount, 2); 143 | 144 | }); 145 | }); 146 | }) 147 | }) 148 | describe("set", function() { 149 | it("when a nucleus attribute is set, its Genotype.update gets triggered automatically", function() { 150 | let $div = document.createElement("div") 151 | $div.Meta = {} 152 | $div.Genotype = { 153 | class: null 154 | } 155 | Nucleus.build($div) 156 | 157 | spy.Genotype.update.reset() 158 | $div.class = "red"; 159 | compare(spy.Genotype.update.callCount, 1); 160 | }) 161 | it("if the attribute is not declared, its Genotype.update DOES NOT get triggered automatically", function() { 162 | let $div = document.createElement("div") 163 | $div.Meta = {} 164 | $div.Genotype = { } 165 | Nucleus.build($div) 166 | 167 | spy.Genotype.update.reset() 168 | $div.class = "red"; 169 | compare(spy.Genotype.update.callCount, 0); 170 | }) 171 | }) 172 | }) 173 | describe("default tracked attributes", function() { 174 | it("tracks $text, $html, $type, $components", function() { 175 | // $type, $text, $components get tracked for change by default 176 | // so that their change auto-triggers $update() 177 | let $div = document.createElement("div") 178 | $div.Meta = {} 179 | $div.Genotype = { } 180 | Nucleus.build($div) 181 | var ret = Nucleus.bind($div, "non-function") 182 | Nucleus.build($div); 183 | compare($div.hasOwnProperty("$type"), true) 184 | compare($div.hasOwnProperty("$text"), true) 185 | compare($div.hasOwnProperty("$html"), true) 186 | compare($div.hasOwnProperty("$components"), true) 187 | }) 188 | }) 189 | }) 190 | describe("bind", function() { 191 | describe("when a non-function is passed", function() { 192 | it("returns a non-function", function() { 193 | let $div = document.createElement("div") 194 | $div.Meta = {} 195 | $div.Genotype = {} 196 | Nucleus.build($div) 197 | var ret = Nucleus.bind($div, "non-function") 198 | compare(typeof ret, "string") 199 | }) 200 | }) 201 | describe("when function is passed", function() { 202 | it("returns a function", function() { 203 | let $div = document.createElement("div") 204 | $div.Meta = {} 205 | $div.Genotype = {} 206 | Nucleus.build($div) 207 | var ret = Nucleus.bind($div, function() { /* something */ }) 208 | compare(typeof ret, "function") 209 | }) 210 | describe("post binding behavior", function() { 211 | let $div; 212 | let oldFun; 213 | beforeEach(function() { 214 | $div = document.createElement("div") 215 | $div.Meta = {} 216 | $div.Genotype = { 217 | $type: "div", 218 | _todo: true, 219 | _done: true, 220 | $update: function() { } 221 | } 222 | oldFun = function(name) { 223 | return "hello, " + name; 224 | }; 225 | oldMutationFun = function(name) { 226 | this._done = false; 227 | return "hello, " + name; 228 | }; 229 | Nucleus.build($div) 230 | }) 231 | it("executes the original function", function() { 232 | let oldFunSpy = sinon.spy(oldFun, "apply"); 233 | var newFun = Nucleus.bind($div, oldFun); 234 | newFun("world") 235 | compare(oldFunSpy.callCount, 1) 236 | }) 237 | it("executes the original function and returns the correct value", function() { 238 | var newFun = Nucleus.bind($div, oldFun); 239 | compare(newFun("world"), "hello, world") 240 | }) 241 | it("empties the queue", function(done) { 242 | var newFun = Nucleus.bind($div, oldFun); 243 | Nucleus._queue = []; 244 | Nucleus._queue.push($div) 245 | newFun("world") 246 | setTimeout(function() { 247 | compare(Nucleus._queue, []) 248 | done() 249 | }, 100) 250 | }) 251 | describe("phenotype.update (not to be confused with $update)", function() { 252 | it("Nucleus.queue doesn't keep duplicates", function() { 253 | Nucleus._queue = [] 254 | for(let i = 0; i<10; i++) { 255 | Nucleus.queue($div) 256 | } 257 | compare(Nucleus._queue.length, 1) 258 | }) 259 | it("calls Phenotype.set if something has changed (not to be confused with $update)", function(done) { 260 | Nucleus._queue = [] 261 | for(let i = 0; i<10; i++) { 262 | Nucleus.queue($div) 263 | } 264 | 265 | spy.Phenotype.set.reset() 266 | 267 | var newMutationFun = Nucleus.bind($div, oldMutationFun); 268 | newMutationFun("world") 269 | 270 | // 10 tasks * 3 keys 271 | setTimeout(function() { 272 | compare(spy.Phenotype.set.callCount, 1) 273 | done() 274 | }, 100) 275 | 276 | }) 277 | it("does not calls Phenotype.set if nothing has changed (not to be confused with $update)", function(done) { 278 | Nucleus._queue = [] 279 | for(let i = 0; i<10; i++) { 280 | Nucleus._queue.push($div) 281 | } 282 | 283 | spy.Phenotype.set.reset() 284 | 285 | var newFun = Nucleus.bind($div, oldFun); 286 | newFun("world") 287 | 288 | // 10 tasks * 3 keys 289 | setTimeout(function() { 290 | compare(spy.Phenotype.set.callCount, 0) 291 | done() 292 | }, 100) 293 | 294 | }) 295 | }) 296 | describe("auto-calling $update", function() { 297 | it("doesn't call $update if there's no $update", function(done) { 298 | Nucleus._queue = [] 299 | delete $div.Genotype.$update 300 | for(let i = 0; i<10; i++) { 301 | Nucleus.queue($div, "_done") 302 | } 303 | 304 | $div.Genotype._done = false; 305 | 306 | spy.Phenotype.set.reset() 307 | spy.Phenotype.$update.reset() 308 | 309 | var newFun = Nucleus.bind($div, oldFun); 310 | newFun("world") 311 | 312 | setTimeout(function() { 313 | compare(spy.Phenotype.$update.callCount, 0) 314 | done() 315 | }, 100) 316 | }) 317 | it("calls Phenotype.$update if a '_' variable is in the queue", function(done) { 318 | spy.Gene.freeze.reset() 319 | Nucleus._queue = [] 320 | for(let i = 0; i<10; i++) { 321 | Nucleus.queue($div, "_done") 322 | Nucleus.queue($div, "_todo") 323 | } 324 | 325 | compare(Nucleus._queue.length, 1) // only one element in the queue 326 | compare(spy.Gene.freeze.callCount, 2) 327 | 328 | $div.Genotype._todo = false; 329 | $div.Genotype._done = false; 330 | 331 | spy.Phenotype.set.reset() 332 | spy.Phenotype.$update.reset() 333 | 334 | var newFun = Nucleus.bind($div, oldFun); 335 | newFun("world") 336 | 337 | setTimeout(function() { 338 | // 1 task * 2 keys 339 | compare(spy.Phenotype.set.callCount, 2) 340 | 341 | // call $update once 342 | compare(spy.Phenotype.$update.callCount, 1) 343 | done() 344 | }, 100) 345 | 346 | }) 347 | describe("sets the queue correctly if $type, $text, $html, or $components is updated", function(done) { 348 | it("$type", function() { 349 | var $d = document.createElement("div"); 350 | var $node = $d.$build({ 351 | $type: "div", 352 | $text: "hi" 353 | }, []) 354 | 355 | $node.$type = "span"; 356 | setTimeout(function() { 357 | compare(spy.Genotype.update.callCount, 1) 358 | compare(Nucleus._queue.length, 1) 359 | compare(Nucleus._queue[0].Genotype, {$type: "span", $text: "hi"}) 360 | compare(Nucleus._queue[0].Dirty, {$type: "dirty"}) 361 | done() 362 | }, 100) 363 | 364 | }) 365 | it("$text", function() { 366 | var $d = document.createElement("div"); 367 | var $node = $d.$build({ 368 | $type: "div", 369 | $text: "hi" 370 | }, []) 371 | 372 | $node.$text = "bye"; 373 | setTimeout(function() { 374 | compare(spy.Genotype.update.callCount, 1) 375 | compare(Nucleus._queue.length, 1) 376 | compare(Nucleus._queue[0].Genotype, {$type: "div", $text: "bye"}) 377 | compare(Nucleus._queue[0].Dirty, {$text: "hi"}) 378 | done() 379 | }, 100) 380 | 381 | }) 382 | it("$html", function() { 383 | var $d = document.createElement("div"); 384 | var $node = $d.$build({ 385 | $type: "div", 386 | $html: "

Hello

" 387 | }, []) 388 | 389 | $node.$html = "

Hello world

"; 390 | setTimeout(function() { 391 | compare(spy.Genotype.update.callCount, 1) 392 | compare(Nucleus._queue.length, 1) 393 | compare(Nucleus._queue[0].Genotype, {$type: "div", $html: "

Hello

"}) 394 | compare(Nucleus._queue[0].Dirty, {$html: "

Hello world

"}) 395 | done() 396 | }, 100) 397 | 398 | }) 399 | it("$components", function() { 400 | var $d = document.createElement("div"); 401 | var $node = $d.$build({ 402 | $type: "div", 403 | $text: "hi" 404 | }, []) 405 | 406 | $node.$components = [{$type: "div", $text: "child"}] 407 | setTimeout(function() { 408 | compare(spy.Genotype.update.callCount, 1) 409 | compare(Nucleus._queue.length, 1) 410 | compare(Nucleus._queue[0].Genotype, {$type: "div", $text: "hi", $components: [{$type: "div", $text: "child"}]}) 411 | compare(Nucleus._queue[0].Dirty, {$components: "[{$type: \"div\", $text: \"child\"}]"}) 412 | done() 413 | }, 100) 414 | 415 | }) 416 | }) 417 | it("doesn't call Phenotype.$update if a '_' variable is NOT in the queue", function(done) { 418 | Nucleus._queue = [] 419 | $div.Genotype = { 420 | $type: "div", 421 | class: 'red' 422 | } 423 | for(let i = 0; i<10; i++) { 424 | Nucleus.queue($div) 425 | } 426 | 427 | spy.Phenotype.set.reset() 428 | spy.Phenotype.$update.reset() 429 | 430 | var newFun = Nucleus.bind($div, oldFun); 431 | newFun("world") 432 | 433 | setTimeout(function() { 434 | // 10 tasks * 2 keys 435 | compare(spy.Phenotype.set.callCount, 0) 436 | 437 | // _todo key exists so call $update for all elements 438 | compare(spy.Phenotype.$update.callCount, 0) 439 | done() 440 | }, 100) 441 | 442 | }) 443 | }) 444 | }) 445 | }) 446 | }) 447 | describe("queue", function() { 448 | it("basic structure", function() { 449 | let $div1 = document.createElement("div") 450 | $div1.Meta = {} 451 | $div1.Genotype = {} 452 | Nucleus.build($div1) 453 | 454 | let $div2 = document.createElement("div") 455 | $div2.Meta = {} 456 | $div2.Genotype = {} 457 | Nucleus.build($div2) 458 | 459 | /* 460 | each task looks like this: 461 | { 462 | $node: $div, 463 | keys: { 464 | $type: null 465 | } 466 | } 467 | */ 468 | 469 | Nucleus._queue = [] 470 | Nucleus.queue($div1, "$type") 471 | Nucleus.queue($div1, "_type") 472 | Nucleus.queue($div2, "_todo") 473 | 474 | assert.equal(Nucleus._queue[0], $div1) 475 | assert.equal(Nucleus._queue[1], $div2) 476 | compare(Nucleus._queue.length, 2) 477 | }) 478 | describe("queueing creates Dirty", function() { 479 | // All 'Dirty' objects are stringified (frozen) versions of the original 480 | it("simple object", function() { 481 | var $div = document.createElement("div") 482 | $div.Meta = {} 483 | $div.Genotype = { 484 | $type: "div", 485 | _todo: true, 486 | _done: true 487 | } 488 | compare($div.Dirty, undefined) 489 | Nucleus.build($div) 490 | Nucleus.queue($div, "_todo") 491 | compare($div.Dirty, {"_todo": "true"}) 492 | Nucleus.queue($div, "_done") 493 | compare($div.Dirty, {"_todo": "true", "_done": "true"}) 494 | }) 495 | it("complex object", function() { 496 | var $div = document.createElement("div") 497 | $div.Meta = {} 498 | $div.Genotype = { 499 | $type: "div", 500 | _items: [1,2,3,4,5], 501 | _index: 2 502 | } 503 | compare($div.Dirty, undefined) 504 | Nucleus.build($div) 505 | Nucleus.queue($div, "_items") 506 | compare($div.Dirty, {"_items": "[1,2,3,4,5]"}) 507 | Nucleus.queue($div, "_index") 508 | compare($div.Dirty, {"_items": "[1,2,3,4,5]", "_index": "2"}) 509 | }) 510 | }) 511 | }) 512 | }) 513 | -------------------------------------------------------------------------------- /test/Phenotype.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const sinon = require('sinon') 3 | const spy = require("./spy.js") 4 | const stringify = require('json-stable-stringify') 5 | const {God, Phenotype, Nucleus} = require("../cell") 6 | const compare = function(actual, expected) { 7 | assert.equal(stringify(actual), stringify(expected)); 8 | } 9 | describe("Phenotype", function() { 10 | require('jsdom-global')() 11 | God.plan(window); 12 | describe("attrs", function() { 13 | describe("$type", function() { 14 | describe("text", function() { 15 | it("empty text node", function() { 16 | const node = Phenotype.$type({$type: "text"}) 17 | compare(node.nodeType, 3) // Node.TEXT_NODE 3 18 | }) 19 | it("text node with content", function() { 20 | const node = Phenotype.$type({$type: "text", $text: "Hello World"}) 21 | compare(node.nodeType, 3) 22 | compare(node.textContent, "Hello World") 23 | compare(typeof node.innerHTML, "undefined") 24 | }) 25 | }) 26 | describe("svg", function() { 27 | it("svg", function() { 28 | const node = Phenotype.$type({$type: "svg"}) 29 | compare(node.tagName.toLowerCase(), "svg") 30 | compare(node.namespaceURI, "http://www.w3.org/2000/svg") 31 | compare(node.Meta, {namespace: "http://www.w3.org/2000/svg"}) 32 | }) 33 | it("svg children", function() { 34 | const node = Phenotype.$type({$type: "p"}, "dummy namespace") 35 | compare(node.namespaceURI, "dummy namespace") 36 | compare(node.Meta, {namespace: "dummy namespace"}) 37 | }) 38 | it("text node as svg descendent", function() { 39 | const node = Phenotype.$type({$type: "text"}, "http://www.w3.org/2000/svg") 40 | // Should not be a text node. Should be a svg text node 41 | compare(node.nodeType === Node.TEXT_NODE, false) 42 | compare(node.namespaceURI, "http://www.w3.org/2000/svg"); 43 | compare(node.Meta, {namespace: "http://www.w3.org/2000/svg"}) 44 | }) 45 | }) 46 | it("fragment", function() { 47 | const node = Phenotype.$type({$type: "fragment"}) 48 | compare(node.nodeType, 11) // Node.DOCUMENT_FRAGMENT_NODE 11 49 | compare(node.Meta, {}) 50 | }) 51 | it("without type => default div", function() { 52 | const node = Phenotype.$type({}) 53 | compare(node.nodeType, 1) // Node.ELEMENT_NODE 1 54 | compare(node.tagName.toLowerCase(), "div") 55 | compare(node.Meta, {}) 56 | }) 57 | describe("with type", function() { 58 | it("basic", function() { 59 | const node = Phenotype.$type({$type: "p"}) 60 | compare(node.nodeType, 1) // Node.ELEMENT_NODE 1 61 | compare(node.tagName.toLowerCase(), "p") 62 | compare(node.Meta, {}) 63 | }) 64 | it("doesn't fill in the text yet", function() { 65 | const node = Phenotype.$type({$type: "p", $text: "hi"}) 66 | compare(node.nodeType, 1) // Node.ELEMENT_NODE 1 67 | compare(node.tagName.toLowerCase(), "p") 68 | compare(node.Meta, {}) 69 | compare(node.innerHTML, ""); 70 | compare(node.outerHTML, "

") 71 | }) 72 | it("doesn't set the attribute yet", function() { 73 | const node = Phenotype.$type({$type: "p", class: "red"}) 74 | compare(node.nodeType, 1) // Node.ELEMENT_NODE 1 75 | compare(node.tagName.toLowerCase(), "p") 76 | compare(node.Meta, {}) 77 | compare(node.getAttribute('class'), null) 78 | compare(node.class, undefined) 79 | }) 80 | }) 81 | }) 82 | describe("html", function() { 83 | it("html node with content", function() { 84 | const node = Phenotype.$type({ $html: '

' }) 85 | compare(node.nodeType, 1); 86 | }) 87 | }) 88 | describe("$components", function() { 89 | it("basic", function() { 90 | var $node = root.document.body.$build({$type: "ul"}, []) 91 | spy.Phenotype.$init.reset(); 92 | Phenotype.$components($node, [{ 93 | $type: "li", 94 | class: "red" 95 | }, { 96 | $type: "li", 97 | class: "green" 98 | }, { 99 | $type: "li", 100 | class: "blue" 101 | }]) 102 | compare($node.outerHTML, "") 103 | compare(spy.Phenotype.$init.callCount, 3) 104 | }) 105 | it("$fragment.$build gets called components number of times", function() { 106 | var $parent = root.document.body.$build({$type: "ul"}, []) 107 | const components = [{ 108 | $type: "li", 109 | class: "red" 110 | }, { 111 | $type: "li", 112 | class: "green" 113 | }, { 114 | $type: "li", 115 | class: "blue" 116 | }] 117 | 118 | // spy 119 | const fragmentSpy = sinon.spy(DocumentFragment.prototype, "$build") 120 | 121 | // Before 122 | compare($parent.innerHTML, "") 123 | compare($parent.outerHTML, "") 124 | 125 | Phenotype.$components($parent, components) 126 | 127 | // After 128 | compare(fragmentSpy.callCount, 3) // loop three times 129 | compare($parent.outerHTML, "") 130 | }) 131 | it("ties each child component's Genotypes onto the current element's $components array", function() { 132 | var $parent = root.document.body.$build({$type: "ul"}, []) 133 | const components = [{ 134 | $type: "li", class: "red" 135 | }, { 136 | $type: "li", class: "green" 137 | }, { 138 | $type: "li", class: "blue" 139 | }] 140 | Phenotype.$components($parent, components) 141 | compare($parent.$components, components) 142 | }) 143 | it("attaches attributes to the inheritance array", function() { 144 | // inheritance is an array of keys inherited down the DOM tree. 145 | // This necessary because we need to monitor all the inherited keys from the descendants 146 | var $parent = root.document.body.$build({ 147 | $type: "ul", 148 | _index: 1, 149 | $components: [{ 150 | $type: "li", class: "red" 151 | }, { 152 | $type: "li", class: "green" 153 | }, { 154 | $type: "li", class: "blue" 155 | }] 156 | }, []) 157 | compare($parent.Inheritance, []) 158 | compare($parent.childNodes[0].Inheritance, ["_index"]) 159 | compare($parent.childNodes[1].Inheritance, ["_index"]) 160 | compare($parent.childNodes[2].Inheritance, ["_index"]) 161 | }) 162 | describe("injecting into existing nodes should remove all childNodes first", function(){ 163 | it("injecting into body", function(){ 164 | document.body.innerHTML = " "; 165 | compare(document.body.childNodes.length, 1); 166 | var gene = { 167 | $type: "body", 168 | $cell: true, 169 | $components: [{ class: "container" }] 170 | } 171 | var $node = root.document.body.$build(gene, []); 172 | // building should end up with 1 node (not 2) because the empty text node has been deleted 173 | compare($node.childNodes.length, 1); 174 | }) 175 | it("injecting into element with id", function(){ 176 | document.body.innerHTML = "
Old
"; 177 | compare(document.body.childNodes.length, 1); 178 | var gene = { 179 | $cell: true, $type: "div", id: "widget", 180 | $components:[{ 181 | $type: "span", 182 | $text: "New" 183 | }] 184 | }; 185 | var $node = root.document.body.$build(gene, []); 186 | // should replace the old content 187 | compare($node.innerHTML, "New"); 188 | // building should end up with 1 node (not 2) because the contents are removed before cell is injected 189 | compare($node.childNodes.length, 1); 190 | }) 191 | }) 192 | }) 193 | describe("$init", function() { 194 | beforeEach(function() { 195 | Nucleus._queue = [] 196 | }) 197 | it("$init should not trigger $update automatically", function() { 198 | const $parent = document.createElement("div"); 199 | const $node = document.createElement("div") 200 | $node.Genotype = { 201 | $type: "div", 202 | $init: function() { 203 | // something 204 | } 205 | } 206 | $node.Meta = {} 207 | $parent.appendChild($node) 208 | 209 | // Bypass setTimeout 210 | const clock = sinon.useFakeTimers(); 211 | 212 | // spy reset 213 | spy.Nucleus.bind.reset() 214 | Phenotype.$init($node) 215 | clock.tick(1); 216 | compare(spy.Phenotype.$update.callCount, 0) // should be called 217 | 218 | // Restore timer 219 | clock.restore(); 220 | }) 221 | }) 222 | describe("$update", function() { 223 | describe("should call Nucleus.$update correctly in normal cases", function() { 224 | it("first time ever", function() { 225 | const $parent = document.createElement("div"); 226 | const $node = document.createElement("div"); 227 | $node.Genotype = { 228 | _counter: 0, 229 | $update: function() { 230 | // something 231 | this._counter = this._counter+1; 232 | } 233 | } 234 | $node.Meta = {} 235 | Nucleus.build($node); 236 | $parent.appendChild($node) 237 | 238 | compare($node._counter, 0) 239 | 240 | const NodeUpdateSpy = sinon.spy($node.Genotype, "$update") 241 | NodeUpdateSpy.reset() 242 | 243 | Phenotype.$update($node) 244 | compare(NodeUpdateSpy.callCount, 1) 245 | compare($node._counter, 1) 246 | }) 247 | }) 248 | it("detached node shouldn't update (!$node.parentNode)", function() { 249 | const $node = document.createElement("div"); 250 | $node.Genotype = { 251 | $update: function() { 252 | // something 253 | } 254 | } 255 | $node.Meta = {} 256 | $node.Nucleus = {$update: function() { }} 257 | 258 | const NucleusUpdateSpy = sinon.spy($node.Nucleus, "$update") 259 | NucleusUpdateSpy.reset() 260 | 261 | Phenotype.$update($node) 262 | compare(NucleusUpdateSpy.callCount, 0) 263 | }) 264 | it("shouldn't call Nucleus.$update if already updated ($node.Meta.$updated)", function() { 265 | const $node = document.createElement("div"); 266 | $node.Genotype = { 267 | $update: function() { 268 | // something 269 | } 270 | } 271 | $node.Meta = {$updated: true} 272 | $node.Nucleus = {$update: function() { }} 273 | 274 | const NucleusUpdateSpy = sinon.spy($node.Nucleus, "$update") 275 | NucleusUpdateSpy.reset() 276 | 277 | Phenotype.$update($node) 278 | compare(NucleusUpdateSpy.callCount, 0) 279 | }) 280 | }) 281 | }) 282 | describe("build", function() { 283 | it("iterates through all keys in the Genotype", function() { 284 | spy.Phenotype.$init.reset(); 285 | spy.Phenotype.set.reset(); 286 | const $node = document.createElement("div") 287 | $node.Meta = {}; 288 | const replaceChildSpy = sinon.spy($node, "replaceChild") 289 | Phenotype.build($node, {$type: "div", $text: "hi", class: "red"}); 290 | compare(spy.Phenotype.set.callCount, 3) // iterates through all 3 keys 291 | compare(replaceChildSpy.callCount, 0) // should not get inside the replace block 292 | compare(spy.Phenotype.$init.callCount, 1) 293 | }) 294 | }) 295 | describe("update", function() { 296 | describe("[$] Reserved variables", function() { 297 | describe("$type", function() { 298 | it("Phenotype.$init called via fragment.$build", function() { 299 | // parent 300 | const $parent = document.createElement("div"); 301 | $parent.setAttribute("class", "wrapper") 302 | 303 | // node 304 | const $node = document.createElement("div") 305 | $node.Genotype = {} 306 | $node.Meta = {} 307 | $parent.appendChild($node) 308 | 309 | // spy 310 | const replaceChildSpy = sinon.spy($parent, "replaceChild") 311 | spy.Phenotype.$init.reset(); 312 | 313 | // run 314 | Phenotype.set($node, "$type", "div") 315 | 316 | // fragment.$build would trigger an $init, 317 | // but in this case we're not supposed to enter that block so the callcount is 0 318 | compare(spy.Phenotype.$init.callCount, 0) 319 | compare(replaceChildSpy.callCount, 0) 320 | compare($parent.outerHTML, "
") 321 | compare($parent.innerHTML, "
") 322 | }) 323 | it("Replace if the type is different", function() { 324 | // parent 325 | const $parent = document.createElement("div"); 326 | $parent.setAttribute("class", "wrapper") 327 | 328 | // node 329 | const $node = document.createElement("div") 330 | $node.Genotype = {"$type": "p"} 331 | $node.Meta = {} 332 | $parent.appendChild($node) 333 | 334 | // spy 335 | const replaceChildSpy = sinon.spy($parent, "replaceChild") 336 | spy.Phenotype.$init.reset(); 337 | 338 | // run 339 | Phenotype.set($node, "$type", "p") 340 | 341 | // $init is called via fragment.$build even if it's not explicitly called 342 | compare(spy.Phenotype.$init.callCount, 1) 343 | compare(replaceChildSpy.callCount, 1) 344 | compare($parent.outerHTML, "

") 345 | compare($parent.innerHTML, "

") 346 | }) 347 | }) 348 | it("$text", function() { 349 | // updates innerHTML 350 | const $parent = document.createElement("div"); 351 | const $node = document.createElement("div") 352 | $node.Genotype = {} 353 | $node.Meta = {} 354 | $parent.appendChild($node) 355 | compare($node.innerHTML, "") 356 | Phenotype.set($node, "$text", "Hello") 357 | compare($node.innerHTML, "Hello") 358 | }) 359 | describe("$components", function() { 360 | it("empty components should also trigger `$components` call", function() { 361 | const $parent = document.createElement("div"); 362 | const $node = document.createElement("div") 363 | $node.Genotype = {} 364 | $node.Meta = {} 365 | $parent.appendChild($node) 366 | 367 | // spy 368 | spy.Phenotype.$components.reset(); 369 | 370 | // Before 371 | compare($node.innerHTML, "") 372 | 373 | Phenotype.set($node, "$components", []) 374 | 375 | // After 376 | compare(spy.Phenotype.$components.callCount, 1) 377 | }) 378 | it("components with more 0 items should trigger `$components` call", function() { 379 | const $parent = document.createElement("div"); 380 | const $node = document.createElement("div") 381 | $node.Genotype = {} 382 | $node.Meta = {} 383 | $parent.appendChild($node) 384 | 385 | // spy 386 | spy.Phenotype.$components.reset(); 387 | 388 | // Before 389 | compare($node.innerHTML, "") 390 | 391 | Phenotype.set($node, "$components", [{$type: "div"}]) 392 | 393 | // After 394 | compare(spy.Phenotype.$components.callCount, 1) 395 | }) 396 | }) 397 | }) 398 | describe("[_] User defined variables", function() { 399 | }) 400 | describe("[ ] dom attributes", function() { 401 | describe("object", function() { 402 | it("style", function() { 403 | const $parent = document.createElement("div"); 404 | const $node = document.createElement("div") 405 | $node.Genotype = {} 406 | $node.Meta = {} 407 | $parent.appendChild($node) 408 | 409 | // normally it's set directly on the DOM as an attribute 410 | var styleAttr = $node.getAttribute("style"); 411 | compare(styleAttr, null); 412 | 413 | var styleProp = $node.style; 414 | compare(Object.getPrototypeOf(styleProp).constructor.name, "CSSStyleDeclaration"); 415 | 416 | Phenotype.set($node, "style", { 417 | backgroundColor: "red", 418 | fontFamily: "Courier" 419 | }) 420 | 421 | styleAttr = $node.getAttribute("style"); 422 | styleProp = $node.style; 423 | 424 | compare(styleAttr, "background-color: red; font-family: Courier;") 425 | compare(styleProp.backgroundColor, "red"); 426 | compare(styleProp.fontFamily, "Courier"); 427 | 428 | compare(Object.getPrototypeOf(styleProp).constructor.name, "CSSStyleDeclaration"); 429 | 430 | }); 431 | }); 432 | describe("string", function() { 433 | it("style", function() { 434 | const $parent = document.createElement("div"); 435 | const $node = document.createElement("div") 436 | $node.Genotype = {} 437 | $node.Meta = {} 438 | $parent.appendChild($node) 439 | 440 | // normally it's set directly on the DOM as an attribute 441 | var styleAttr = $node.getAttribute("style"); 442 | compare(styleAttr, null); 443 | 444 | var styleProp = $node.style; 445 | compare(Object.getPrototypeOf(styleProp).constructor.name, "CSSStyleDeclaration"); 446 | 447 | Phenotype.set($node, "style", "background-color: red;") 448 | 449 | styleAttr = $node.getAttribute("style"); 450 | styleProp = $node.style; 451 | 452 | compare(styleAttr, "background-color: red;") 453 | // even if we initially set the style as string, 454 | // we should be able to access it as an object property 455 | compare(styleProp.backgroundColor, "red"); 456 | compare(Object.getPrototypeOf(styleProp).constructor.name, "CSSStyleDeclaration"); 457 | 458 | }); 459 | it("class", function() { 460 | const $parent = document.createElement("div"); 461 | const $node = document.createElement("div") 462 | $node.Genotype = {} 463 | $node.Meta = {} 464 | $parent.appendChild($node) 465 | 466 | // normally it's set directly on the DOM as an attribute 467 | compare($node.getAttribute("class"), null) 468 | compare($node.class, undefined) 469 | 470 | Phenotype.set($node, "class", "red") 471 | 472 | compare($node.getAttribute("class"), "red") 473 | compare($node.class, undefined) 474 | }) 475 | it("value for pre-populated input", function() { 476 | //Phenotype.set($node, "value", "bye") 477 | const $parent = document.createElement("div"); 478 | const $node = document.createElement("input") 479 | $node.value = "preset"; 480 | $node.Genotype = {} 481 | $node.Meta = {} 482 | $parent.appendChild($node) 483 | 484 | // normally it's set directly on the DOM as an attribute 485 | compare($node.value, "preset") 486 | 487 | Phenotype.set($node, "value", "reset") 488 | 489 | compare($node.value, "reset") 490 | 491 | }) 492 | it("value for empty input", function() { 493 | //Phenotype.set($node, "value", "bye") 494 | const $parent = document.createElement("div"); 495 | const $node = document.createElement("input") 496 | $node.Genotype = {} 497 | $node.Meta = {} 498 | $parent.appendChild($node) 499 | 500 | // normally it's set directly on the DOM as an attribute 501 | compare($node.getAttribute("value"), null) 502 | compare($node.value, "") 503 | 504 | Phenotype.set($node, "value", "newval") 505 | 506 | compare($node.value, "newval") 507 | }) 508 | }) 509 | it("number", function() { 510 | const $parent = document.createElement("div"); 511 | const $node = document.createElement("div") 512 | $node.Genotype = {} 513 | $node.Meta = {} 514 | $parent.appendChild($node) 515 | 516 | // Before 517 | compare($node.getAttribute("data-id"), null) 518 | compare($node["data-id"], undefined) 519 | 520 | Phenotype.set($node, "data-id", 1) 521 | 522 | // After 523 | compare($node.getAttribute("data-id"), "1") // only set to the DOM attribute (as string) 524 | compare($node["data-id"], undefined) // the property should be undefined 525 | }) 526 | it("boolean", function() { 527 | const $parent = document.createElement("div"); 528 | const $node = document.createElement("div") 529 | $node.Genotype = {} 530 | $node.Meta = {} 531 | $parent.appendChild($node) 532 | 533 | // Before 534 | compare($node.getAttribute("data-done"), null) 535 | compare($node["data-done"], undefined) 536 | 537 | Phenotype.set($node, "data-done", true) 538 | 539 | // After 540 | compare($node.getAttribute("data-done"), "true") // only set to the DOM attribute (as string) 541 | compare($node["data-done"], undefined) // the property should be undefined 542 | }) 543 | it("function (only native HTMLElement methods are supported)", function() { 544 | const $parent = document.createElement("div"); 545 | const $node = document.createElement("div") 546 | $node.Genotype = {} 547 | $node.Meta = {} 548 | $parent.appendChild($node) 549 | 550 | // Before 551 | compare($node.getAttribute("onclick"), null) 552 | 553 | spy.O.getOwnPropertyDescriptor.reset(); 554 | 555 | Phenotype.set($node, "onclick", function(arg) { 556 | return "fun " + arg; 557 | }) 558 | 559 | // After 560 | compare($node.getAttribute("onclick"), null) // Doesn't exist as a DOM attribute 561 | compare(spy.O.getOwnPropertyDescriptor.callCount, 1); // tries once for HTMLElement and finds onclick so only one time trial. 562 | 563 | 564 | // NON HTMLElement method set 565 | spy.O.getOwnPropertyDescriptor.reset(); 566 | Phenotype.set($node, "fun", function(arg) { 567 | return "fun " + arg; 568 | }) 569 | compare($node.getAttribute("fun"), null) // Doesn't exist as a DOM attribute 570 | compare(spy.O.getOwnPropertyDescriptor.callCount, 2); // tries both for HTMLElement and Element, because `fun` doesn't exist. 571 | 572 | }) 573 | }) 574 | }) 575 | }) 576 | -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const sinon = require('sinon') 3 | const stringify = require('json-stable-stringify') 4 | const {God, Phenotype, Genotype, Nucleus} = require("../cell") 5 | const spy = require("./spy.js") 6 | const compare = function(actual, expected) { 7 | assert.equal(stringify(actual), stringify(expected)); 8 | } 9 | const cleanup = function(){ 10 | require('jsdom-global')(); 11 | } 12 | cleanup() 13 | God.plan(window); 14 | 15 | describe("DOM prototype overrides", function() { 16 | beforeEach(cleanup); 17 | 18 | it("$snapshot", function() { 19 | window.c = { 20 | $cell: true, 21 | _model: [], 22 | id: "el", 23 | onclick: function(e) { console.log("clicked"); }, 24 | _fun: function(message) { return "Fun " + message; } 25 | } 26 | compare(document.body.outerHTML, ""); 27 | God.create(window); 28 | var fun = document.body.querySelector("#el")._fun; 29 | compare(fun.snapshot.toString(), "function (message) { return \"Fun \" + message; }"); 30 | 31 | var onclick = document.body.querySelector("#el").Genotype.onclick; 32 | compare(onclick.snapshot.toString(), "function (e) { console.log(\"clicked\"); }"); 33 | 34 | var snapshot = document.body.querySelector("#el").$snapshot(); 35 | compare(snapshot._fun.toString(), "function (message) { return \"Fun \" + message; }"); 36 | compare(snapshot.onclick.toString(), "function (e) { console.log(\"clicked\"); }"); 37 | }) 38 | 39 | }); 40 | describe("Nucleus", function() { 41 | beforeEach(cleanup); 42 | 43 | it("has nothing at the beginning", function() { 44 | God.create(window); 45 | compare(document.body.outerHTML, ""); 46 | }) 47 | it("God.create creates correct markup", function() { 48 | window.c = { 49 | $cell: true, 50 | _model: [], 51 | id: "grandparent", 52 | $components: [{ 53 | id: "parent", 54 | $components: [{ 55 | id: "child" 56 | }] 57 | }, { 58 | $type: "div", 59 | id: "aunt" 60 | }] 61 | } 62 | compare(document.body.outerHTML, ""); 63 | God.create(window); 64 | compare(document.body.outerHTML, "
") 65 | }) 66 | it("God.create triggers God.detect, the detect correctly detects", function() { 67 | window.c = { 68 | $cell: true, 69 | _model: [], 70 | id: "grandparent", 71 | $components: [{ 72 | id: "parent", 73 | $components: [{ 74 | id: "child" 75 | }] 76 | }, { 77 | $type: "div", 78 | id: "aunt" 79 | }] 80 | } 81 | spy.God.detect.reset(); 82 | const bodySpy = sinon.spy(document.body, "$build") 83 | God.create(window); 84 | compare(bodySpy.callCount, 1); 85 | compare(spy.God.detect.callCount, 1); 86 | }) 87 | describe("context inheritance", function() { 88 | beforeEach(cleanup); 89 | 90 | it("walks up the DOM tree to find the attribute if it doesn't exist on the current node", function() { 91 | window.c = { 92 | $cell: true, 93 | _model: [1,2,3], 94 | id: "grandparent", 95 | $components: [{ 96 | id: "parent", 97 | $components: [{ 98 | id: "child" 99 | }] 100 | }, { 101 | $type: "div", 102 | id: "aunt" 103 | }] 104 | } 105 | God.create(window); 106 | var $child = document.body.querySelector("#child") 107 | compare($child._model, [1,2,3]); 108 | }) 109 | it("finds the attribute on the current element first", function() { 110 | window.c = { 111 | $cell: true, 112 | _model: [1,2,3], 113 | id: "grandparent", 114 | $components: [{ 115 | id: "parent", 116 | $components: [{ 117 | id: "child", 118 | _model: ["a"] 119 | }] 120 | }, { 121 | $type: "div", 122 | id: "aunt" 123 | }] 124 | } 125 | God.create(window); 126 | var $child = document.body.querySelector("#child") 127 | compare($child._model, ["a"]); 128 | }) 129 | it("descendants can share an ancestor's variable", function() { 130 | window.c = { 131 | $cell: true, 132 | _model: [1,2,3], 133 | id: "grandparent", 134 | $components: [{ 135 | id: "parent", 136 | $components: [{ 137 | id: "child" 138 | }] 139 | }, { 140 | $type: "div", 141 | id: "aunt" 142 | }] 143 | } 144 | God.create(window); 145 | var $child = document.body.querySelector("#child") 146 | var $aunt = document.body.querySelector("#aunt") 147 | 148 | // update _model from child 149 | $child._model.push("from child"); 150 | compare($child._model, [1,2,3,"from child"]); 151 | 152 | // access _model from aunt (same as above) 153 | compare($aunt._model, [1,2,3,"from child"]) 154 | }) 155 | }) 156 | }) 157 | 158 | describe("Infects with nice viruses", function() { 159 | beforeEach(cleanup); 160 | 161 | it("can have an update_propagating_virus", function() { 162 | function update_propagating_virus(component){ 163 | let recursive_update = (node) => { 164 | for(let n of node.children){ 165 | n.$update && n.$update() 166 | recursive_update(n) 167 | } 168 | } 169 | 170 | let old_update = component.$update 171 | 172 | component.$update = function(){ 173 | old_update && old_update.call(this) 174 | recursive_update(this) 175 | } 176 | 177 | return component 178 | } 179 | 180 | window.c = { 181 | $cell: true, 182 | $type: 'ul', 183 | _name: '', 184 | $virus: update_propagating_virus, 185 | $components: [ 186 | { $type: 'li', 187 | $components: [ 188 | { $type: 'p', 189 | $text: '', 190 | $update: function(){ 191 | this.$text = this._name; 192 | } 193 | }, 194 | { $type: 'p', $text: 'other' } 195 | ] 196 | } 197 | ] 198 | } 199 | 200 | compare(document.body.outerHTML, "") 201 | God.create(window) 202 | 203 | var $node = document.body.querySelector("ul") 204 | $node._name = "infected" 205 | Phenotype.$update($node) 206 | compare(document.body.querySelector("p").$text, 'infected') 207 | }) 208 | it("can have a markup helper virus", function(){ 209 | function expand_selector(component, selector){ 210 | let parts = selector.match(/([a-zA-Z0-9]*)([#a-zA-Z0-9-_]*)([.a-zA-Z0-9-_]*)/) 211 | if (parts[1]) component.$type = parts[1] 212 | if (parts[2]) component.id = parts[2].substring(1) 213 | if (parts[3]) component['class'] = parts[3].split('.').join(' ').trim() 214 | return component 215 | } 216 | 217 | function hamlism(component){ 218 | if(component.$components){ 219 | component.$components = component.$components.map(hamlism) 220 | } 221 | 222 | let tag = component.$tag 223 | if(!tag) return component 224 | 225 | selectors = tag.split(' ') 226 | expand_selector(component, selectors.pop()) 227 | 228 | return selectors.reduceRight(function(child, selector){ 229 | return expand_selector({$components: [child]}, selector) 230 | }, component) 231 | } 232 | 233 | window.c = { 234 | $cell: true, 235 | $tag: '.class-a span#id-span.class-b', 236 | $virus: [ hamlism ], 237 | $components: [{ $tag: 'li#main.list-item' }] 238 | } 239 | 240 | compare(document.body.outerHTML, "") 241 | God.create(window) 242 | compare(document.body.outerHTML, 243 | ''+ 244 | '
' + 245 | ''+ 246 | '
  • '+ 247 | '
    '+ 248 | '
    '+ 249 | '' 250 | ) 251 | }) 252 | }) 253 | -------------------------------------------------------------------------------- /test/spy.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const {Phenotype, Genotype, Nucleus, Membrane, Gene, God} = require("../cell") 3 | module.exports = { 4 | Genotype: { 5 | set: sinon.spy(Genotype, "set"), 6 | update: sinon.spy(Genotype, "update") 7 | }, 8 | O: { 9 | defineProperty: sinon.spy(Object, "defineProperty"), 10 | getOwnPropertyDescriptor: sinon.spy(Object, "getOwnPropertyDescriptor") 11 | }, 12 | Gene: { 13 | freeze: sinon.spy(Gene, "freeze") 14 | }, 15 | Membrane: { 16 | inject: sinon.spy(Membrane, "inject"), 17 | add: sinon.spy(Membrane, "add") 18 | }, 19 | God: { 20 | create: sinon.spy(God, "create"), 21 | detect: sinon.spy(God, "detect") 22 | }, 23 | Phenotype: { 24 | $init: sinon.spy(Phenotype, "$init"), 25 | $update: sinon.spy(Phenotype, "$update"), 26 | $type: sinon.spy(Phenotype, "$type"), 27 | $components: sinon.spy(Phenotype, "$components"), 28 | set: sinon.spy(Phenotype, "set") 29 | }, 30 | Nucleus: { 31 | bind: sinon.spy(Nucleus, "bind"), 32 | queue: sinon.spy(Nucleus, "queue") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /travis/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # For revert branches, do nothing 5 | if [[ "$TRAVIS_BRANCH" == revert-* ]]; then 6 | echo -e "\e[36m\e[1mTest triggered for reversion branch \"${TRAVIS_BRANCH}\" - doing nothing." 7 | exit 0 8 | fi 9 | 10 | # For PRs 11 | if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then 12 | echo -e "\e[36m\e[1mTest triggered for PR #${TRAVIS_PULL_REQUEST}." 13 | fi 14 | 15 | # Figure out the source of the test 16 | if [ -n "$TRAVIS_TAG" ]; then 17 | echo -e "\e[36m\e[1mTest triggered for tag \"${TRAVIS_TAG}\"." 18 | else 19 | echo -e "\e[36m\e[1mTest triggered for branch \"${TRAVIS_BRANCH}\"." 20 | fi 21 | 22 | # Run the tests 23 | npm test 24 | -------------------------------------------------------------------------------- /website/components/mailchimp/form.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    Subscribe to Newsletter

    5 |

    Stay updated on milestones, new projects, and useful tips from the community

    6 |
    7 | 8 |
    9 |
    10 | 11 | 12 |
    13 | 14 |
    15 | 16 |
    17 |
    18 |
    19 |
    20 | 21 | -------------------------------------------------------------------------------- /website/demos/bitcoin.js: -------------------------------------------------------------------------------- 1 | /**************************************************** 2 | Bitcoin Ticker powered by wss://api.bitfinex.com/ws 3 | *****************************************************/ 4 | Item = function(o){ 5 | return { 6 | class: "row hidden", 7 | $init: function(){ 8 | var t = this; 9 | setTimeout(function(){ t._display() }, 200) 10 | }, 11 | _display: function(){ this.class = "row" }, 12 | $components: [ 13 | { $type: "h1", $text: "$" + o.dollars }, 14 | { class: "timestamp", _timestamp: o.timestamp, $text: Timeago(o.timestamp), _refresh: function(){ 15 | this.$text = Timeago(this._timestamp) 16 | }} 17 | ] 18 | } 19 | } 20 | 21 | Btn = { 22 | $type: "a", $text: "View Source", target: "_blank", href: "https://gliechtenstein.github.io/bitcoin.cell.js/bitcoin.js", 23 | style: "position: absolute; top:10px; right:10px; padding: 8px 15px; border: 1px solid rgba(255,255,255,0.2); text-decoration: none; color: rgba(255,255,255,0.9); font-weight: normal; font-size: 12px; border-radius: 4px;" 24 | } 25 | 26 | Bitcoin = function($el){ 27 | var ws = new WebSocket('wss://api.bitfinex.com/ws') 28 | ws.addEventListener('message', function (event) { 29 | if(Array.isArray(JSON.parse(event.data))) $el._add(JSON.parse(event.data)) 30 | }) 31 | ws.addEventListener('open', function (event) { 32 | ws.send(JSON.stringify({ "event":"subscribe", "channel":"ticker", "pair":"BTCUSD" })) 33 | }) 34 | return ws; 35 | } 36 | 37 | Container = { 38 | class: "container", 39 | $init: function(){ this._bitcoin = Bitcoin(this) }, 40 | $components: [], 41 | _add: function(data){ 42 | if(data[1] !== "hb"){ 43 | this.$components.unshift(Item({dollars: data[1], timestamp: Date.now()})) 44 | this.querySelectorAll(".timestamp").forEach(function(timestamp){ timestamp._refresh() }) 45 | } 46 | } 47 | } 48 | 49 | Timeago = function(d){ return timeago().format(d) } 50 | 51 | $root = { 52 | $cell: true, id: "widget", style: "position: relative;", 53 | $components: [Container, Btn] 54 | } 55 | -------------------------------------------------------------------------------- /website/demos/twitter.css: -------------------------------------------------------------------------------- 1 | #twitter .avatar{ 2 | width: 80px; height: 80px; margin-right: 10px; border-radius: 40px; 3 | } 4 | #twitter h5{ 5 | font-size: 18px; margin: 0 0 10px; color: #FFA100; 6 | } 7 | #twitter .container{ 8 | padding: 50px; max-width: 800px; margin: 0 auto; position: relative; 9 | } 10 | #twitter .item{ 11 | font-family: "HelveticaNeue", helvetica; display: flex; font-size: 14px; color: rgba(255,255,255,0.8); padding: 20px; 12 | -moz-transition: all 2s ease-in-out; 13 | -webkit-transition: all 2s ease-in-out; 14 | -ms-transition: all 2s ease-in-out; 15 | -o-transition: all 2s ease-in-out; 16 | transition: all 2s ease-in-out; 17 | opacity: 1; 18 | top: -100px; 19 | max-width: 2000px; 20 | } 21 | #twitter .body img{ 22 | width: 100%; margin-top: 20px; 23 | } 24 | #twitter .item.hidden{ 25 | opacity: 0; top: 0px; max-height: 0; 26 | } 27 | -------------------------------------------------------------------------------- /website/demos/twitter.js: -------------------------------------------------------------------------------- 1 | var T = { $cell: true, $components: [ 2 | { $type: "link", href: "./website/demos/twitter.css", rel: "stylesheet" }, 3 | { $components: [], class: "container", id: "twitter", 4 | _add: function(data){ this.$components = [this._mediaItem(data)].concat(this.$components).slice(0,50) }, 5 | $init: function(){ 6 | var ws = new WebSocket('wss://twitsocket.herokuapp.com').addEventListener('message', function (event) { this._add(JSON.parse(event.data)) }.bind(this)) }, 7 | _mediaItem: function(data){ 8 | return { class: "item hidden", $init: function(){ this.class = "item" }, 9 | $components: [ 10 | { style: "display: block;", $components: [{$type: "img", class: "avatar", src: data.user.profile_image_url_https }] }, 11 | { class: "body", $components: [ 12 | { $type: "h5", $text: data.user.name + " (" + data.user.screen_name + ")" }, { $type: "text", $text: data.text } 13 | ].concat( data.extended_entities && data.extended_entities.media ? [{ $type: "img", src: data.extended_entities.media[0].media_url_https }] : [] ) } ] } } } ]} 14 | -------------------------------------------------------------------------------- /website/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | padding-top: 100px; 3 | } 4 | hr { 5 | padding: 20px; 6 | border: none; 7 | } 8 | input.email { 9 | width: 100%; 10 | padding: 10px; 11 | font-size: 14px; 12 | } 13 | #mailchimp { 14 | max-width: 500px; 15 | border-radius: 5px; 16 | background: #f0f0f0; 17 | padding: 20px 20px 10px; 18 | margin: 0 auto; 19 | } 20 | .align-right{ 21 | text-align: right; 22 | } 23 | .logo { 24 | text-align: center; 25 | } 26 | .logo img { 27 | width: 300px; 28 | } 29 | a.badge { 30 | display: none; 31 | } 32 | a.badge img { 33 | width: 80px; 34 | } 35 | img.logo { 36 | width: 100%; 37 | max-width: 700px; 38 | display: block; 39 | margin: 0 auto; 40 | } 41 | img.avatar { 42 | width: 50px; 43 | } 44 | table { 45 | margin: 20px 0; 46 | table-layout: fixed; 47 | width: 100%; 48 | } 49 | #twitter { 50 | overflow: auto; 51 | } 52 | .header { 53 | text-align: center; 54 | padding: 20px; 55 | } 56 | blockquote { 57 | margin: 10px 0; 58 | padding: 0 15px; 59 | color: rgba(0,0,0,0.8); 60 | border-left: 3px solid rgba(0,0,0,0.8); 61 | } 62 | body { 63 | max-width: 800px; 64 | font-family: HelveticaNeue, helvetica, arial; 65 | width: 100%; 66 | margin: 10px auto; 67 | padding: 20px; 68 | box-sizing: border-box; 69 | color: rgba(0,0,0,0.7); 70 | } 71 | .btn { 72 | padding: 5px 10px; 73 | text-transform: uppercase; 74 | font-size: 12px; 75 | border-radius: 40px; 76 | text-decoration: none; 77 | margin: 5px; 78 | display: inline-block; 79 | } 80 | .btn-primary { 81 | border: 2px solid rgba(0,0,0,0.8); 82 | color: white; 83 | background: rgba(0,0,0,0.8); 84 | } 85 | .btn-secondary { 86 | border: 2px solid rgba(0,0,0,0.8); 87 | color: rgba(0,0,0,0.8); 88 | } 89 | pre { 90 | font-size: 14px; 91 | line-height: 16px; 92 | word-wrap: break-word; 93 | white-space: pre-wrap; 94 | } 95 | pre code { 96 | background: #f0f0f0 !important; 97 | display: block; 98 | padding: 20px !important; 99 | } 100 | code { 101 | border-radius: 2px; 102 | padding: 0px 3px !important; 103 | background: #f0f0f0 !important; 104 | } 105 | td { 106 | font-family: 'Raleway', sans-serif; 107 | font-weight: 500; 108 | font-size: 14px; 109 | padding: 10px; 110 | margin: 0; 111 | border: none; 112 | background: whitesmoke; 113 | } 114 | th { 115 | padding: 10px; 116 | margin: 0; 117 | font-size: 14px; 118 | border: 1px solid whitesmoke; 119 | } 120 | h1 { 121 | color: black; 122 | font-family: 'Raleway', sans-serif; 123 | font-size: 40px; 124 | font-weight: 300; 125 | margin-top: 40px !important; 126 | margin-bottom: 20px !important; 127 | } 128 | img { 129 | width: 100% 130 | } 131 | div.content { 132 | background: white; 133 | padding: 100px 10px; 134 | } 135 | p { 136 | font-family: 'Raleway', sans-serif; 137 | font-weight: 500; 138 | font-size: 14px; 139 | line-height: 20px; 140 | } 141 | h2 { 142 | color: black; 143 | font-family: 'Merriweather', serif; 144 | font-size: 20px; 145 | font-weight: 500; 146 | line-height: 35px; 147 | margin-top: 30px; 148 | margin-bottom: 20px; 149 | } 150 | h3 { 151 | color: black; 152 | font-size: 18px; 153 | } 154 | h3 a { 155 | color: rgba(0,0,0,0.8); 156 | text-decoration: underline; 157 | padding: 20px 0; 158 | display: inline-block; 159 | } 160 | a { 161 | font-weight: bold; 162 | color: #0366d6; 163 | } 164 | ol { 165 | padding: 20px; 166 | } 167 | h4 { 168 | margin: 0; 169 | } 170 | .mc-field-group { 171 | margin: 10px 0; 172 | } 173 | .mc-field-group input { 174 | border: none; 175 | outline: none; 176 | } 177 | h5 { 178 | font-family: Merriweather; 179 | font-size: 14px; 180 | } 181 | li { 182 | font-family: 'Raleway', sans-serif; 183 | font-weight: 500; 184 | font-size: 14px; 185 | line-height: 18px; 186 | margin-bottom: 10px; 187 | } 188 | li:before { 189 | content: \\0BB \\020; 190 | } 191 | ul { 192 | list-style: none; 193 | -webkit-padding-start: 20px; 194 | } 195 | #widget{ 196 | background: #191919; 197 | max-height: 400px; 198 | padding-top: 20px; 199 | overflow: auto; 200 | border-radius: 10px; 201 | } 202 | #widget h1{ 203 | margin: 0; 204 | color: white; 205 | font-size: 60px; 206 | text-align: center; 207 | width: 100%; 208 | display: block; 209 | } 210 | #widget .timestamp{ 211 | font-size: 12px; 212 | text-align: center; 213 | width: 100%; 214 | color: gray; 215 | } 216 | #widget .row{ 217 | padding: 10px; 218 | margin: 10px; 219 | -webkit-transition: all 1s ease-out, opacity 1s ease-out; 220 | -moz-transition: all 1s ease-out, opacity 1s ease-out; 221 | transition: all 1s ease-out, opacity 1s ease-out; 222 | overflow:hidden; 223 | border-bottom: 1px solid rgba(255,255,255,0.04); 224 | padding-bottom: 20px; 225 | max-height: 600px; 226 | opacity: 1; 227 | } 228 | #widget .row.hidden{ 229 | opacity: 0; 230 | max-height: 0; 231 | } 232 | #twitter .container{ 233 | width: 500px; 234 | max-width: 100%; 235 | } 236 | #twitter .item{ 237 | position: relative; 238 | padding: 10px; 239 | margin: 10px; 240 | max-height: 600px; 241 | -moz-transition: all 2s ease-in-out; 242 | -webkit-transition: all 2s ease-in-out; 243 | -ms-transition: all 2s ease-in-out; 244 | -o-transition: all 2s ease-in-out; 245 | transition: all 2s ease-in-out; 246 | top: 0px; 247 | opacity: 1; 248 | overflow: hidden; 249 | } 250 | #twitter .item.hidden{ 251 | opacity: 0; 252 | top: -100px; 253 | max-height: 0px; 254 | } 255 | #twitter{ 256 | background: #191919; 257 | font-size: 12px; 258 | color: rgba(255,255,255,0.8); 259 | max-height: 400px; 260 | border-radius: 10px; 261 | } 262 | #twitter h5{ 263 | color: #FFA100; 264 | font-family: Merriweather, serif; 265 | font-size: 17px; 266 | } 267 | #twitter img.avatar { 268 | border-radius: 24px; 269 | } 270 | #twitter .media-body img{ 271 | width: 100%; 272 | margin: 10px; 273 | box-sizing: border-box; 274 | } 275 | --------------------------------------------------------------------------------