├── .gitignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── baseComponent.js ├── binding.js ├── containerComponent.js ├── domComponents.js ├── fancyProps.js ├── firmer.js ├── genericComponent.js ├── images ├── fastn-sml.png └── fastn.png ├── index.browser.js ├── index.js ├── is.js ├── listComponent.js ├── package.json ├── property.js ├── schedule.js ├── templaterComponent.js ├── test ├── attach.js ├── binding.js ├── changes.js ├── component.js ├── components.js ├── container.js ├── createFastn.js ├── customBinding.js ├── customModel.js ├── document.js ├── fancyProps.js ├── firmer.js ├── generic.js ├── index.html ├── index.js ├── list.js ├── property.js ├── templater.js └── text.js └── textComponent.js /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.browser.js 2 | node_modules 3 | *.log -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | addons: 5 | apt: 6 | packages: 7 | - xvfb 8 | install: 9 | - export DISPLAY=':99.0' 10 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 11 | - npm install 12 | script: npm run test -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for fastn 2 | 3 | 4 | ## v2 5 | 6 | Version two of fastn changes the way the component constructors work, to allow for better composition of components. 7 | 8 | In fastn v1, component constructors would create a component, modify it, then return it. 9 | 10 | In fastn v2, component constructors are passed a component, and they may extend it with functionality. 11 | 12 | The v1 way: 13 | 14 | ``` 15 | 16 | function myCoolComponentConstructor(fastn, type, settings, children){ 17 | var component = fastn.base(type, settings, children); 18 | 19 | // Add properties, implement/override methods, etc... 20 | 21 | return component; 22 | } 23 | 24 | ``` 25 | 26 | The v2 way: 27 | 28 | ``` 29 | 30 | function myCoolComponentConstructor(fastn, component, type, settings, children){ 31 | // Add properties, implement/override methods, etc... 32 | 33 | return component; 34 | } 35 | 36 | ``` 37 | 38 | ## Extending 39 | 40 | In v1, if you wanted to make a component that was an extension of another component, you would do something like this: 41 | 42 | ``` 43 | 44 | function myCoolFancyList(fastn, type, settings, children){ 45 | 46 | // Create a list. 47 | var component = fastn.createComponent('list', settings, children); 48 | 49 | // Add properties, implement/override methods, etc... 50 | 51 | return component; 52 | } 53 | 54 | ``` 55 | 56 | in v2, you can just call .extend()... 57 | 58 | 59 | ``` 60 | 61 | function myCoolComponentConstructor(fastn, component, type, settings, children){ 62 | 63 | // Become a list. 64 | component.extend('list', settings, children); 65 | 66 | // Add properties, implement/override methods, etc... 67 | 68 | return component; 69 | } 70 | 71 | ``` 72 | 73 | Which is extremely handy if you want features from multiple components: 74 | 75 | 76 | ``` 77 | 78 | function myCoolComponentConstructor(fastn, component, type, settings, children){ 79 | 80 | // Become a list. 81 | component.extend('list', settings, children); 82 | 83 | // Also be a modal 84 | component.extend('modal', settings, children); 85 | 86 | // Also be a whatever 87 | component.extend('whatever', settings, children); 88 | 89 | // Add properties, implement/override methods, etc... 90 | 91 | return component; 92 | } 93 | 94 | ``` 95 | 96 | # Why 97 | 98 | In fastn v2, you can mix components together when you create them, like so: 99 | 100 | ``` 101 | var myMapList = fastn('list:map', { ... }); 102 | ``` 103 | 104 | Fastn will, under the covers, extend all the types together in the order they are listed, so the above example is equivilent to: 105 | 106 | fastn('list', settings, children...).extend('map', settings, children...); 107 | 108 | ## Details 109 | 110 | ### API 111 | 112 | #### Removed 113 | 114 | - fastn.createComponent 115 | 116 | #### Changed 117 | 118 | - componant constructor parameters (fastn, type, settings, children) -> (fastn, component, type, settings, children) 119 | - component.setProperty can now be passed only a key, which will use the existing property, or create a new default one for that key. 120 | 121 | #### Added 122 | 123 | - Mixin syntax, fastn('componantType1:componantType2') 124 | - componant.extend(componantType, settings, children) 125 | - componant.is(componantType) -> bool 126 | - fastn.componants._container is now defaulted to containerComponant. 127 | 128 | ### Best Practice 129 | 130 | #### Composition 131 | 132 | In v1, you could add functionality to a componant arbitrarily, with no real structure 133 | 134 | In v2, obviously, using the fastn('foo:bar') style is recommended. 135 | 136 | #### Adding properties 137 | 138 | In v1, properties were generally added via 139 | 140 | ``` 141 | property.addTo(componant, key); 142 | ``` 143 | 144 | In v2 this is deprecated, and it is encouraged that you instead use: 145 | 146 | ``` 147 | componant.setProperty('key', property); 148 | ``` 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastn 2 | 3 | Create ultra-lightweight UI components 4 | 5 | [![Build Status](https://travis-ci.org/KoryNunn/fastn.svg?branch=master)](https://travis-ci.org/KoryNunn/fastn) 6 | [![Join the chat at https://gitter.im/KoryNunn/fastn](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KoryNunn/fastn?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | ![fastn](http://korynunn.github.io/fastn/images/fastn-sml.png) 9 | 10 | ## [Homepage](http://korynunn.github.io/fastn/), [Try it](http://korynunn.github.io/fastn/try/), [Example app](http://korynunn.github.io/fastn/example/) 11 | 12 | # Usage 13 | 14 | The absolute minimum required to make a fastn component: 15 | 16 | initialise fastn with default DOM components: 17 | 18 | ```javascript 19 | var fastn = require('fastn')( 20 | // Default components for rendering DOM. 21 | require('fastn/domComponents')(/* optional extra constructors */) 22 | ); 23 | ``` 24 | 25 | Or use your own selection of constructors: 26 | 27 | ```javascript 28 | // Require and initialise fastn 29 | var fastn = require('fastn')({ 30 | // component constructors.. Add what you need to use 31 | 32 | text: require('fastn/textComponent'), // Renders text 33 | _generic: require('fastn/genericComponent') // Renders DOM nodes 34 | }); 35 | ``` 36 | 37 | Make components: 38 | ```javascript 39 | var something = fastn('h1', 'Hello World'); 40 | 41 | ``` 42 | 43 | Put them on the screen: 44 | ```javascript 45 | something.render(); 46 | 47 | window.addEventListener('load', function(){ 48 | document.body.appendChild(something.element); 49 | }); 50 | ``` 51 | [^ try it](http://korynunn.github.io/fastn/try/#InJldHVybiBmYXN0bignaDEnLCAnSGVsbG8gV29ybGQnKTsi) 52 | 53 | `fastn` is a function with the signature: 54 | 55 | ```javascript 56 | fastn(type[, settings, children...]) 57 | ``` 58 | 59 | which can be used to create a UI: 60 | 61 | ```javascript 62 | // Create some component 63 | var someComponent = fastn('section', 64 | fastn('h1', 'I\'m a component! :D'), 65 | fastn('a', {href: 'http://google.com'}, 'An anchor') 66 | ); 67 | 68 | someComponent.render(); 69 | 70 | // Append the components element to the DOM 71 | document.body.appendChild(someComponent.element); 72 | ``` 73 | [^ try it](http://korynunn.github.io/fastn/try/#InJldHVybiBmYXN0bignc2VjdGlvbicsXG5cdGZhc3RuKCdoMScsICdJXFwnbSBhIGNvbXBvbmVudCEgOkQnKSxcblx0ZmFzdG4oJ2EnLCB7aHJlZjogJ2h0dHA6Ly9nb29nbGUuY29tJ30sICdBbiBhbmNob3InKVxuKTsi) 74 | 75 | You can assign bindings to properties: 76 | 77 | ```javascript 78 | 79 | var someComponent = fastn('section', 80 | fastn('h1', 'I\'m a component! :D'), 81 | fastn('a', {href: fastn.binding('url')}, 82 | fastn('label', 'This link points to '), 83 | fastn('label', fastn.binding('url')) 84 | ) 85 | ); 86 | 87 | someComponent.attach({ 88 | url: 'http://google.com' 89 | }); 90 | 91 | ``` 92 | 93 | Which can be updated via a number of methods. 94 | 95 | 96 | ```javascript 97 | 98 | someComponent.scope().set('url', 'http://bing.com'); 99 | 100 | 101 | ``` 102 | [^ try it](http://korynunn.github.io/fastn/try/#InZhciBzb21lQ29tcG9uZW50ID0gZmFzdG4oJ3NlY3Rpb24nLFxuICAgICAgICBmYXN0bignaDEnLCAnSVxcJ20gYSBjb21wb25lbnQhIDpEJyksXG4gICAgICAgIGZhc3RuKCdhJywge2hyZWY6IGZhc3RuLmJpbmRpbmcoJ3VybCcpfSxcbiAgICAgICAgICAgIGZhc3RuKCdsYWJlbCcsICdUaGlzIGxpbmsgcG9pbnRzIHRvICcpLFxuICAgICAgICAgICAgZmFzdG4oJ2xhYmVsJywgZmFzdG4uYmluZGluZygndXJsJykpXG4gICAgICAgIClcbiAgICApO1xuXG5zb21lQ29tcG9uZW50LmF0dGFjaCh7XG4gICAgdXJsOiAnaHR0cDovL2dvb2dsZS5jb20nXG59KTtcblxuc2V0VGltZW91dChmdW5jdGlvbigpe1xuXHRzb21lQ29tcG9uZW50LnNjb3BlKCkuc2V0KCd1cmwnLCAnaHR0cDovL2JpbmcuY29tJyk7XG59LCAyMDAwKTtcblxucmV0dXJuIHNvbWVDb21wb25lbnQ7Ig==) 103 | 104 | ## Special component types 105 | 106 | There are a few special component types that are used as shorthands for some situations: 107 | 108 | ### `text` 109 | 110 | if a string or `binding` is added as a child into a containerComponent, fastn will look for a `text` component, set it's `text` to the string or `binding`, and insert it. This is handy as you don't need to write: `fastn('text', 'foo')` all over the place. 111 | [^ try it](http://korynunn.github.io/fastn/try/#InJldHVybiBmYXN0bignZGl2Jyxcblx0ZmFzdG4oJ3RleHQnLCB7dGV4dDogJ0V4cGxpY2l0IHRleHQsICd9KSxcblx0J0ltcGxpY2l0IHRleHQsICcsXG4gICAgZmFzdG4uYmluZGluZygnYm91bmRUZXh0JylcbikuYXR0YWNoKHtcbiAgXHRib3VuZFRleHQ6ICdCb3VuZCB0ZXh0J1xufSk7Ig==) 112 | 113 | ### `_generic` 114 | 115 | If the type passed to fastn does not exactly match any known components, fastn will check for a `_generic` component, and pass all the settings and children through to it. 116 | [^ try it](http://korynunn.github.io/fastn/try/#InJldHVybiBmYXN0bignZGl2JyxcbiAgICAgICAgICAgICBcblx0ZmFzdG4oJ3NwYW4nLCAnV29vIGEgc3BhbiEnKSxcbiAgICAgICAgICAgICBcblx0ZmFzdG4oJ2JyJyksIC8vIEJyIGJlY2F1c2Ugd2UgY2FuIVxuICAgICAgICAgICAgIFxuICAgIGZhc3RuKCdhJywge2hyZWY6ICdodHRwczovL2dpdGh1Yi5jb20va29yeW51bm4vZmFzdG4nfSwgJ0FuIGFuY2hvcicpLFxuICAgICAgICAgICAgIFxuXHRmYXN0bignYnInKSwgLy8gQW5vdGhlciBiciBmb3IgcmVhc29uc1xuICAgICAgICAgICAgIFxuICAgIGZhc3RuKCdpbWcnLCB7dGl0bGU6ICdBd2Vzb21lIGxvZ28nLCBzcmM6ICdodHRwOi8va29yeW51bm4uZ2l0aHViLmlvL2Zhc3RuL3RyeS9mYXN0bi1zbWwucG5nJ30pXG4pOyI=) 117 | 118 | ## Default components 119 | 120 | fastn includes 4 extremely simple default components that render as DOM nodes. It is not necessary to use them, and you can replace them with your own, enabling you to render to anything you want to. 121 | 122 | ### textComponent 123 | 124 | A default handler for the `text` component type that renders a textNode. e.g.: 125 | 126 | ```javascript 127 | fastn('something', // render a thing 128 | 'Some string passed as a child' // falls into the `text` component, renders as a textNode 129 | ) 130 | ``` 131 | 132 | ### genericComponent 133 | 134 | A default handler for the `_generic` component type that renders DOM nodes based on the type passed, e.g.: 135 | 136 | ```javascript 137 | fastn('div') // no component is assigned to 'div', fastn will search for _generic, and if this component is assigned to it, it will create a div element. 138 | ``` 139 | 140 | ### listComponent 141 | 142 | Takes a template and inserts children based on the result of its `items` property, e.g.: 143 | 144 | ```javascript 145 | fastn('list', { 146 | items: [1,2,3], 147 | template: function(){ 148 | return fastn.binding('item') 149 | } 150 | }) 151 | ``` 152 | Templated components will be attached to a model that contains `key` and `item`, where `key` is the key in the set that they correspond to, and `item` is the data of the item in the set. 153 | 154 | #### Lazy templating 155 | 156 | If you need to render a huge list of items, and you're noticing a UI hang, you can choose to enable 157 | lazy templating by setting a lists' `insertionFrameTime` to some value: 158 | 159 | ```javascript 160 | fastn('list', { 161 | insertionFrameTime: 32, // Only render items for 32 milliseconds at a time before awaiting idle time. 162 | items: [1,2,3], 163 | template: function(){ 164 | return fastn.binding('item') 165 | } 166 | }) 167 | ``` 168 | 169 | ### templaterComponent 170 | 171 | Takes a template and replaces itself with the component rendered by the template. Returning null from the template indicates that nothing should be inserted. 172 | 173 | The template function will be passed the last component that was rendered by it as the third parameter. 174 | 175 | Note: The template function will run immediately upon component creation. This means that if your data is a binding, the first run of the template function will always receive undefined as it's item. 176 | 177 | ```javascript 178 | fastn('templater', { 179 | data: 'foo', 180 | template: function(model, scope, currentComponent){ 181 | if(model.get('item') === 'foo'){ 182 | return fastn('img'); 183 | }else{ 184 | return null; 185 | } 186 | } 187 | }) 188 | ``` 189 | 190 | An optional property of `attachTemplates` can be provided. When set to true (default), the children rendered from the template will be attached to a new scope that contains the templater data, under the key of `item`. 191 | 192 | When set to false, the children rendered from the template will inherit their attachment from the templator. 193 | 194 | ```javascript 195 | var appData = { 196 | foo: 'bar' 197 | }; 198 | 199 | fastn('templater', { 200 | data: binding('foo'), 201 | attachTemplates: false, 202 | template: function(model, scope, currentComponent){ 203 | // model.get('item') -> is appData 204 | } 205 | }).attach(appData) 206 | ``` 207 | 208 | ## A little deeper.. 209 | 210 | A component can be created by calling `fastn` with a `type`, like so: 211 | 212 | ```javascript 213 | var myComponent = fastn('myComponent'); 214 | ``` 215 | 216 | This will create a component registered in `components` with the key `'myComponent'` 217 | 218 | If `'myComponent'` is not found, fastn will check for a `'_generic'` constructor, and use that if defined. The generic component will create a DOM element of the given type passed in, and is likely the most common component you will create. 219 | 220 | ```javascript 221 | var divComponent = fastn('div', {'class':'myDiv'}); 222 | ``` 223 | 224 | The above will create a `component`, that renders as a `div` with a class of `'myDiv'` 225 | 226 | __the default genericComponent will automatically convert all keys in the settings object to properties.__ 227 | 228 | ## `fastn.binding(key)` 229 | 230 | Creates a binding with the given key. 231 | 232 | A binding can be attached to data using `.attach(object)`. 233 | 234 | 235 | # The Bits.. 236 | 237 | There are very few parts to fastn, they are: 238 | 239 | `component`, `property`, and `binding` 240 | 241 | If you are just want to render some DOM, you will probably be able to just use the default ones. 242 | 243 | ## `component` 244 | 245 | A fastn `component` is an object that represents a chunk of UI. 246 | 247 | ```javascript 248 | 249 | var someComponent = fastn('componentType', settings (optional), children (optional)...) 250 | 251 | ``` 252 | 253 | ## `property` 254 | 255 | A fastn `property` is a getterSetter function and EventEmitter. 256 | 257 | ```javascript 258 | 259 | var someProperty = fastn.property(defaultValue, changes (optional), updater (optional)); 260 | 261 | // get it's value 262 | someProperty(); // returns it's value; 263 | 264 | // set it's value 265 | someProperty(anything); // sets anything and returns the property. 266 | 267 | // add a change handler 268 | someProperty.on('change', function(value){ 269 | // value is the properties new value. 270 | }); 271 | 272 | ``` 273 | 274 | Properties can be added to components in a number of ways: 275 | 276 | via the settings object: 277 | 278 | ```javascript 279 | 280 | var component = fastn('div', { 281 | property: someProperty 282 | }); 283 | 284 | ``` 285 | at a later point via property.addTo(component, key); 286 | 287 | ```javascript 288 | 289 | someProperty.addTo(component, 'someProperty'); 290 | 291 | ``` 292 | 293 | ## `binding` 294 | 295 | A fastn `binding` is a getterSetter function and `EventEmitter`. 296 | 297 | It is used as a mapping between an object and a key or path on that object. 298 | 299 | The path syntax is identical to that used in [enti](https://github.com/KoryNunn/enti#paths) 300 | 301 | ```javascript 302 | 303 | var someBinding = fastn.binding('foo'); 304 | 305 | // get it's value 306 | someBinding(); // returns it's value; 307 | 308 | // set it's value 309 | someBinding(anything); // sets anything and returns the binding. 310 | 311 | // add a change handler 312 | someBinding.on('change', function(value){ 313 | // value is the properties new value. 314 | }); 315 | 316 | ``` 317 | 318 | You can pass multiple paths or other bindings to a binding, along with a fuse function, to combine them into a single result: 319 | 320 | ```javascript 321 | 322 | var anotherBinding = fastn.binding('bar', 'baz', someBinding, function(bar, baz, foo){ 323 | return bar + foo; 324 | }); 325 | 326 | ``` 327 | 328 | ### `binding.from(value)` 329 | 330 | - if value is a binding: return `value`, 331 | - else: return a binding who's value is `value`. 332 | 333 | useful when you don't know what something is, but you need it in a binding: 334 | 335 | ```javascript 336 | 337 | var someBinding = fastn.binding('someKey', fastn.binding.from(couldBeAnything), function(someValue, valueOfAnything){ 338 | 339 | }); 340 | 341 | ``` 342 | 343 | ### A note on the difference between `properties` and `bindings` 344 | 345 | On the surface, properties and bindings look very similar. 346 | They can both be used like getter/setter functions, and they both emit change events. 347 | 348 | They differ both in usage and implementation in that properties don't have any awareness of a model or paths, 349 | and bindings don't have any awareness of components. 350 | 351 | This distinction shines when you design your application with 'services' or 'controllers' that encapsulate models and how to interact with them. 352 | Check out the example applications [search service](https://github.com/KoryNunn/fastn/blob/gh-pages/example/search.js) and 353 | [search bar component](https://github.com/KoryNunn/fastn/blob/gh-pages/example/searchBar.js). 354 | The service only deals with data, and the component only deals with UI. 355 | 356 | # Browser Support 357 | 358 | Fastn works in all the latest evergreen browsers. 359 | 360 | Fastn *May* work in other browsers, but will almost certainly need a few polyfills like WeakMap, Map, WeakSet, Set, etc... 361 | -------------------------------------------------------------------------------- /baseComponent.js: -------------------------------------------------------------------------------- 1 | var is = require('./is'), 2 | GENERIC = '_generic', 3 | EventEmitter = require('events').EventEmitter, 4 | slice = Array.prototype.slice; 5 | 6 | function flatten(item){ 7 | return Array.isArray(item) ? item.reduce(function(result, element){ 8 | if(element == null){ 9 | return result; 10 | } 11 | return result.concat(flatten(element)); 12 | },[]) : item; 13 | } 14 | 15 | function attachProperties(object, firm){ 16 | for(var key in this._properties){ 17 | this._properties[key].attach(object, firm); 18 | } 19 | } 20 | 21 | function onRender(){ 22 | 23 | // Ensure all bindings are somewhat attached just before rendering 24 | this.attach(undefined, 0); 25 | 26 | for(var key in this._properties){ 27 | this._properties[key].update(); 28 | } 29 | } 30 | 31 | function detachProperties(firm){ 32 | for(var key in this._properties){ 33 | this._properties[key].detach(firm); 34 | } 35 | } 36 | 37 | function destroyProperties(){ 38 | for(var key in this._properties){ 39 | this._properties[key].destroy(); 40 | } 41 | } 42 | 43 | function clone(){ 44 | return this.fastn(this.component._type, this.component._settings, this.component._children.filter(function(child){ 45 | return !child._templated; 46 | }).map(function(child){ 47 | return typeof child === 'object' ? child.clone() : child; 48 | }) 49 | ); 50 | } 51 | 52 | function getSetBinding(newBinding){ 53 | if(!arguments.length){ 54 | return this.binding; 55 | } 56 | 57 | if(!is.binding(newBinding)){ 58 | newBinding = this.fastn.binding(newBinding); 59 | } 60 | 61 | if(this.binding && this.binding !== newBinding){ 62 | this.binding.removeListener('change', this.emitAttach); 63 | newBinding.attach(this.binding._model, this.binding._firm); 64 | } 65 | 66 | this.binding = newBinding; 67 | 68 | this.binding.on('change', this.emitAttach); 69 | this.binding.on('detach', this.emitDetach); 70 | 71 | this.emitAttach(); 72 | 73 | return this.component; 74 | }; 75 | 76 | function emitAttach(){ 77 | var newBound = this.binding(); 78 | if(newBound !== this.lastBound){ 79 | this.lastBound = newBound; 80 | this.scope.attach(this.lastBound); 81 | this.component.emit('attach', this.scope, 1); 82 | } 83 | } 84 | 85 | function emitDetach(){ 86 | this.component.emit('detach', 1); 87 | } 88 | 89 | function getScope(){ 90 | return this.scope; 91 | } 92 | 93 | function destroy(){ 94 | if(this.destroyed){ 95 | return; 96 | } 97 | this.destroyed = true; 98 | 99 | this.component 100 | .removeAllListeners('render') 101 | .removeAllListeners('attach'); 102 | 103 | this.component.emit('destroy'); 104 | this.component.element = null; 105 | this.scope.destroy(); 106 | this.binding.destroy(true); 107 | 108 | return this.component; 109 | } 110 | 111 | function attachComponent(object, firm){ 112 | this.binding.attach(object, firm); 113 | return this.component; 114 | } 115 | 116 | function detachComponent(firm){ 117 | this.binding.detach(firm); 118 | return this.component; 119 | } 120 | 121 | function isDestroyed(){ 122 | return this.destroyed; 123 | } 124 | 125 | function setProperty(key, property){ 126 | 127 | // Add a default property or use the one already there 128 | if(!property){ 129 | property = this.component[key] || this.fastn.property(); 130 | } 131 | 132 | this.component[key] = property; 133 | this.component._properties[key] = property; 134 | 135 | return this.component; 136 | } 137 | 138 | function bindInternalProperty(component, model, propertyName, propertyTransform){ 139 | if(!(propertyName in component)){ 140 | component.setProperty(propertyName); 141 | } 142 | component[propertyName].on('change', function(value){ 143 | model.set(propertyName, propertyTransform ? propertyTransform(value) : value); 144 | }); 145 | } 146 | 147 | function createInternalScope(data, propertyTransforms){ 148 | var componentScope = this; 149 | var model = new componentScope.fastn.Model(data); 150 | 151 | for(var key in data){ 152 | bindInternalProperty(componentScope.component, model, key, propertyTransforms[key]); 153 | } 154 | 155 | return { 156 | binding: function(){ 157 | return componentScope.fastn.binding.apply(null, arguments).attach(model); 158 | }, 159 | model: model 160 | }; 161 | } 162 | 163 | function extendComponent(type, settings, children){ 164 | var component = this.component; 165 | 166 | if(type in this.types){ 167 | return component; 168 | } 169 | 170 | if(!(type in this.fastn.components)){ 171 | 172 | if(!(GENERIC in this.fastn.components)){ 173 | throw new Error('No component of type "' + type + '" is loaded'); 174 | } 175 | 176 | component = this.fastn.components._generic(this.fastn, this.component, type, settings, children, createInternalScope.bind(this)); 177 | 178 | if(component){ 179 | this.types._generic = true; 180 | } 181 | }else{ 182 | 183 | component = this.fastn.components[type](this.fastn, this.component, type, settings, children, createInternalScope.bind(this)); 184 | } 185 | 186 | if(component){ 187 | this.types[type] = true; 188 | } 189 | 190 | return component; 191 | }; 192 | 193 | function isType(type){ 194 | return type in this.types; 195 | } 196 | 197 | function FastnComponent(fastn, type, settings, children){ 198 | var component = this; 199 | 200 | var componentScope = { 201 | types: {}, 202 | fastn: fastn, 203 | component: component, 204 | binding: fastn.binding('.'), 205 | destroyed: false, 206 | scope: new fastn.Model(false), 207 | lastBound: null 208 | }; 209 | 210 | componentScope.emitAttach = emitAttach.bind(componentScope); 211 | componentScope.emitDetach = emitDetach.bind(componentScope); 212 | componentScope.binding._default_binding = true; 213 | 214 | component._type = type; 215 | component._properties = {}; 216 | component._settings = settings || {}; 217 | component._children = children ? flatten(children) : []; 218 | 219 | component.attach = attachComponent.bind(componentScope); 220 | component.detach = detachComponent.bind(componentScope); 221 | component.scope = getScope.bind(componentScope); 222 | component.destroy = destroy.bind(componentScope); 223 | component.destroyed = isDestroyed.bind(componentScope); 224 | component.binding = getSetBinding.bind(componentScope); 225 | component.setProperty = setProperty.bind(componentScope); 226 | component.clone = clone.bind(componentScope); 227 | component.children = slice.bind(component._children); 228 | component.extend = extendComponent.bind(componentScope); 229 | component.is = isType.bind(componentScope); 230 | 231 | component.binding(componentScope.binding); 232 | 233 | component.on('attach', attachProperties.bind(this)); 234 | component.on('render', onRender.bind(this)); 235 | component.on('detach', detachProperties.bind(this)); 236 | component.on('destroy', destroyProperties.bind(this)); 237 | 238 | if(fastn.debug){ 239 | component.on('render', function(){ 240 | if(component.element && typeof component.element === 'object'){ 241 | component.element._component = component; 242 | } 243 | }); 244 | } 245 | } 246 | FastnComponent.prototype = Object.create(EventEmitter.prototype); 247 | FastnComponent.prototype.constructor = FastnComponent; 248 | FastnComponent.prototype._fastn_component = true; 249 | 250 | module.exports = FastnComponent; -------------------------------------------------------------------------------- /binding.js: -------------------------------------------------------------------------------- 1 | var is = require('./is'), 2 | firmer = require('./firmer'), 3 | functionEmitter = require('function-emitter'), 4 | setPrototypeOf = require('setprototypeof'), 5 | same = require('same-value'); 6 | 7 | function noop(x){ 8 | return x; 9 | } 10 | 11 | function fuseBinding(){ 12 | var fastn = this, 13 | args = Array.prototype.slice.call(arguments); 14 | 15 | var bindings = args.slice(), 16 | transform = bindings.pop(), 17 | updateTransform, 18 | resultBinding = createBinding.call(fastn), 19 | selfChanging; 20 | 21 | resultBinding._arguments = args; 22 | 23 | if(typeof bindings[bindings.length-1] === 'function' && !is.binding(bindings[bindings.length-1])){ 24 | updateTransform = transform; 25 | transform = bindings.pop(); 26 | } 27 | 28 | resultBinding._model.removeAllListeners(); 29 | resultBinding._set = function(value){ 30 | if(updateTransform){ 31 | selfChanging = true; 32 | var newValue = updateTransform(value); 33 | if(!same(newValue, bindings[0]())){ 34 | bindings[0](newValue); 35 | resultBinding._change(newValue); 36 | } 37 | selfChanging = false; 38 | }else{ 39 | resultBinding._change(value); 40 | } 41 | }; 42 | 43 | function change(){ 44 | if(selfChanging){ 45 | return; 46 | } 47 | resultBinding(transform.apply(null, bindings.map(function(binding){ 48 | return binding(); 49 | }))); 50 | } 51 | 52 | resultBinding.on('detach', function(firm){ 53 | bindings.forEach(function(binding, index){ 54 | binding.detach(firm); 55 | }); 56 | }); 57 | 58 | resultBinding.once('destroy', function(soft){ 59 | bindings.forEach(function(binding, index){ 60 | binding.removeListener('change', change); 61 | binding.destroy(soft); 62 | }); 63 | }); 64 | 65 | bindings.forEach(function(binding, index){ 66 | if(!is.binding(binding)){ 67 | binding = createBinding.call(fastn, binding); 68 | bindings[index] = binding; 69 | } 70 | binding.on('change', change); 71 | }); 72 | 73 | var lastAttached; 74 | resultBinding.attach = function(object, firm){ 75 | if(firmer(resultBinding, firm)){ 76 | return resultBinding; 77 | } 78 | 79 | resultBinding._firm = firm; 80 | 81 | selfChanging = true; 82 | bindings.forEach(function(binding){ 83 | binding.attach(object, 1); 84 | }); 85 | selfChanging = false; 86 | if(lastAttached !== object){ 87 | change(); 88 | } 89 | lastAttached = object; 90 | resultBinding._model.attach(object); 91 | resultBinding.emit('attach', object, firm); 92 | return resultBinding; 93 | } 94 | 95 | return resultBinding; 96 | } 97 | 98 | function createValueBinding(fastn){ 99 | var valueBinding = createBinding.call(fastn, 'value'); 100 | valueBinding.attach = function(){return valueBinding;}; 101 | valueBinding.detach = function(){return valueBinding;}; 102 | return valueBinding; 103 | } 104 | 105 | function bindingTemplate(newValue){ 106 | if(!arguments.length){ 107 | return this.value; 108 | } 109 | 110 | if(this.binding._fastn_binding === '.'){ 111 | return; 112 | } 113 | 114 | this.binding._set(newValue); 115 | return this.binding; 116 | } 117 | 118 | function modelAttachHandler(data){ 119 | var bindingScope = this; 120 | bindingScope.binding._model.attach(data); 121 | bindingScope.binding._change(bindingScope.binding._model.get(bindingScope.path)); 122 | bindingScope.binding.emit('attach', data, 1); 123 | } 124 | 125 | function modelDetachHandler(){ 126 | this.binding._model.detach(); 127 | } 128 | 129 | function attach(object, firm){ 130 | var bindingScope = this; 131 | var binding = bindingScope.binding; 132 | // If the binding is being asked to attach loosly to an object, 133 | // but it has already been defined as being firmly attached, do not attach. 134 | if(firmer(binding, firm)){ 135 | return binding; 136 | } 137 | 138 | binding._firm = firm; 139 | 140 | var isModel = bindingScope.fastn.isModel(object); 141 | 142 | if(isModel && bindingScope.attachedModel === object){ 143 | return binding; 144 | } 145 | 146 | if(bindingScope.attachedModel){ 147 | bindingScope.attachedModel.removeListener('attach', bindingScope.modelAttachHandler); 148 | bindingScope.attachedModel.removeListener('detach', bindingScope.modelDetachHandler); 149 | bindingScope.attachedModel = null; 150 | } 151 | 152 | if(isModel){ 153 | bindingScope.attachedModel = object; 154 | bindingScope.attachedModel.on('attach', bindingScope.modelAttachHandler); 155 | bindingScope.attachedModel.on('detach', bindingScope.modelDetachHandler); 156 | object = object._model; 157 | } 158 | 159 | if(binding._model._model === object){ 160 | return binding; 161 | } 162 | 163 | bindingScope.modelAttachHandler(object); 164 | 165 | return binding; 166 | }; 167 | 168 | function detach(firm){ 169 | if(firmer(this.binding, firm)){ 170 | return this.binding; 171 | } 172 | 173 | this.value = undefined; 174 | if(this.binding._model.isAttached()){ 175 | this.binding._model.detach(); 176 | } 177 | this.binding.emit('detach', 1); 178 | return this.binding; 179 | } 180 | 181 | function set(newValue){ 182 | var bindingScope = this; 183 | if(same(bindingScope.binding._model.get(bindingScope.path), newValue)){ 184 | return; 185 | } 186 | if(!bindingScope.binding._model.isAttached()){ 187 | bindingScope.binding._model.attach(bindingScope.binding._model.get('.')); 188 | } 189 | bindingScope.binding._model.set(bindingScope.path, newValue); 190 | } 191 | 192 | function change(newValue){ 193 | var bindingScope = this; 194 | if(newValue === undefined && bindingScope.value === newValue && !bindingScope.binding._model._model){ 195 | return; 196 | } 197 | bindingScope.value = newValue; 198 | bindingScope.binding.emit('change', bindingScope.binding()); 199 | } 200 | 201 | function clone(keepAttachment){ 202 | var bindingScope = this; 203 | var newBinding = createBinding.apply(bindingScope.fastn, bindingScope.binding._arguments); 204 | 205 | if(keepAttachment){ 206 | newBinding.attach(bindingScope.attachedModel || bindingScope.binding._model._model, bindingScope.binding._firm); 207 | } 208 | 209 | return newBinding; 210 | } 211 | 212 | function destroy(soft){ 213 | var bindingScope = this; 214 | if(bindingScope.isDestroyed){ 215 | return; 216 | } 217 | if(soft){ 218 | return; 219 | } 220 | bindingScope.isDestroyed = true; 221 | bindingScope.binding.emit('destroy', true); 222 | bindingScope.binding.detach(); 223 | bindingScope.binding._model.destroy(); 224 | } 225 | 226 | function destroyed(){ 227 | return this.isDestroyed; 228 | } 229 | 230 | function createBinding(path, more){ 231 | var fastn = this; 232 | 233 | if(more){ // used instead of arguments.length for performance 234 | return fuseBinding.apply(fastn, arguments); 235 | } 236 | 237 | if(is.binding(path)){ 238 | return createBinding.call(this, path, noop); 239 | } 240 | 241 | if(arguments.length === 0){ 242 | return createValueBinding(fastn); 243 | } 244 | 245 | if(!(typeof path === 'string' || typeof path === 'number')){ 246 | throw new Error('Invalid path for fastn.binding(String/Number), saw: ', JSON.stringify(path)) 247 | } 248 | 249 | var bindingScope = { 250 | fastn: fastn, 251 | path: path 252 | }, 253 | binding = bindingScope.binding = bindingTemplate.bind(bindingScope); 254 | 255 | setPrototypeOf(binding, functionEmitter); 256 | binding.setMaxListeners(10000); 257 | binding._arguments = [path]; 258 | binding._model = new fastn.Model(false); 259 | binding._fastn_binding = path; 260 | binding._firm = -Infinity; 261 | 262 | bindingScope.modelAttachHandler = modelAttachHandler.bind(bindingScope); 263 | bindingScope.modelDetachHandler = modelDetachHandler.bind(bindingScope); 264 | 265 | binding.attach = attach.bind(bindingScope); 266 | binding.detach = detach.bind(bindingScope); 267 | binding._set = set.bind(bindingScope); 268 | binding._change = change.bind(bindingScope); 269 | binding.clone = clone.bind(bindingScope); 270 | binding.destroy = destroy.bind(bindingScope); 271 | binding.destroyed = destroyed.bind(bindingScope); 272 | 273 | if(path !== '.'){ 274 | binding._model.on(path, binding._change); 275 | } 276 | 277 | return binding; 278 | } 279 | 280 | function from(valueOrBinding){ 281 | if(is.binding(valueOrBinding)){ 282 | return valueOrBinding; 283 | } 284 | 285 | var result = this(); 286 | result(valueOrBinding) 287 | 288 | return result; 289 | } 290 | 291 | module.exports = function(fastn){ 292 | var binding = createBinding.bind(fastn); 293 | binding.from = from.bind(binding); 294 | return binding; 295 | }; 296 | -------------------------------------------------------------------------------- /containerComponent.js: -------------------------------------------------------------------------------- 1 | function insertChild(fastn, container, child, index){ 2 | if(child == null || child === false){ 3 | return; 4 | } 5 | 6 | if(child.destroyed && child.destroyed()){ 7 | throw new Error('Attempted to mount a destroyed componet. Are you re-using a componet that you stored in a variable?') 8 | } 9 | 10 | var currentIndex = container._children.indexOf(child), 11 | newComponent = fastn.toComponent(child); 12 | 13 | if(newComponent !== child && ~currentIndex){ 14 | container._children.splice(currentIndex, 1, newComponent); 15 | } 16 | 17 | if(!~currentIndex || newComponent !== child){ 18 | newComponent.attach(container.scope(), 1); 19 | } 20 | 21 | if(currentIndex !== index){ 22 | if(~currentIndex){ 23 | container._children.splice(currentIndex, 1); 24 | } 25 | container._children.splice(index, 0, newComponent); 26 | } 27 | 28 | if(container.element){ 29 | if(!newComponent.element){ 30 | newComponent.render(); 31 | } 32 | container._insert(newComponent.element, index); 33 | newComponent.emit('insert', container); 34 | container.emit('childInsert', newComponent); 35 | } 36 | } 37 | 38 | function getContainerElement(){ 39 | return this.containerElement || this.element; 40 | } 41 | 42 | function insert(child, index){ 43 | var childComponent = child, 44 | container = this.container, 45 | fastn = this.fastn; 46 | 47 | if(index && typeof index === 'object'){ 48 | childComponent = Array.prototype.slice.call(arguments); 49 | } 50 | 51 | if(isNaN(index)){ 52 | index = container._children.length; 53 | } 54 | 55 | if(Array.isArray(childComponent)){ 56 | for (var i = 0; i < childComponent.length; i++) { 57 | container.insert(childComponent[i], i + index); 58 | } 59 | }else{ 60 | insertChild(fastn, container, childComponent, index); 61 | } 62 | 63 | return container; 64 | } 65 | 66 | module.exports = function(fastn, component, type, settings, children){ 67 | component.insert = insert.bind({ 68 | container: component, 69 | fastn: fastn 70 | }); 71 | 72 | component._insert = function(element, index){ 73 | var containerElement = component.getContainerElement(); 74 | if(!containerElement){ 75 | return; 76 | } 77 | 78 | if(containerElement.childNodes[index] === element){ 79 | return; 80 | } 81 | 82 | containerElement.insertBefore(element, containerElement.childNodes[index]); 83 | }; 84 | 85 | component.remove = function(childComponent){ 86 | var index = component._children.indexOf(childComponent); 87 | if(~index){ 88 | component._children.splice(index,1); 89 | } 90 | 91 | childComponent.detach(1); 92 | 93 | if(childComponent.element){ 94 | component._remove(childComponent.element); 95 | childComponent.emit('remove', component); 96 | } 97 | component.emit('childRemove', childComponent); 98 | }; 99 | 100 | component._remove = function(element){ 101 | var containerElement = component.getContainerElement(); 102 | 103 | if(!element || !containerElement || element.parentNode !== containerElement){ 104 | return; 105 | } 106 | 107 | containerElement.removeChild(element); 108 | }; 109 | 110 | component.empty = function(){ 111 | while(component._children.length){ 112 | component.remove(component._children.pop()); 113 | } 114 | }; 115 | 116 | component.replaceChild = function(oldChild, newChild){ 117 | var index = component._children.indexOf(oldChild); 118 | 119 | if(!~index){ 120 | return; 121 | } 122 | 123 | component.remove(oldChild); 124 | component.insert(newChild, index); 125 | }; 126 | 127 | component.getContainerElement = getContainerElement.bind(component); 128 | 129 | component.on('render', component.insert.bind(null, component._children, 0)); 130 | 131 | component.on('attach', function(model, firm){ 132 | for(var i = 0; i < component._children.length; i++){ 133 | if(fastn.isComponent(component._children[i])){ 134 | component._children[i].attach(model, firm); 135 | } 136 | } 137 | }); 138 | 139 | component.on('destroy', function(data, firm){ 140 | for(var i = 0; i < component._children.length; i++){ 141 | if(fastn.isComponent(component._children[i])){ 142 | component._children[i].destroy(firm); 143 | } 144 | } 145 | }); 146 | 147 | return component; 148 | }; -------------------------------------------------------------------------------- /domComponents.js: -------------------------------------------------------------------------------- 1 | module.exports = function(extra){ 2 | var components = { 3 | // The _generic component is a catch-all for any component type that 4 | // doesnt match any other component constructor, eg: 'div' 5 | _generic: require('./genericComponent'), 6 | 7 | // The text component is used to render text or bindings passed as children to other components. 8 | text: require('./textComponent'), 9 | 10 | // The list component is used to render items based on a set of data. 11 | list: require('./listComponent'), 12 | 13 | // The templater component is used to render one item based on some value. 14 | templater: require('./templaterComponent') 15 | }; 16 | 17 | if(extra){ 18 | Object.keys(extra).forEach(function(key){ 19 | components[key] = extra[key]; 20 | }); 21 | } 22 | 23 | return components; 24 | } -------------------------------------------------------------------------------- /fancyProps.js: -------------------------------------------------------------------------------- 1 | var setify = require('setify'), 2 | classist = require('classist'); 3 | 4 | function updateTextProperty(generic, element, value){ 5 | if(arguments.length === 2){ 6 | return element.textContent; 7 | } 8 | element.textContent = (value == null ? '' : value); 9 | } 10 | 11 | module.exports = { 12 | class: function(generic, element, value){ 13 | if(!generic._classist){ 14 | generic._classist = classist(element); 15 | } 16 | 17 | if(arguments.length < 3){ 18 | return generic._classist(); 19 | } 20 | 21 | generic._classist(value); 22 | }, 23 | display: function(generic, element, value){ 24 | if(arguments.length === 2){ 25 | return element.style.display !== 'none'; 26 | } 27 | element.style.display = value ? null : 'none'; 28 | }, 29 | disabled: function(generic, element, value){ 30 | if(arguments.length === 2){ 31 | return element.hasAttribute('disabled'); 32 | } 33 | if(value){ 34 | element.setAttribute('disabled', 'disabled'); 35 | }else{ 36 | element.removeAttribute('disabled'); 37 | } 38 | }, 39 | innerHTML: function(generic, element, value){ 40 | if(arguments.length === 2){ 41 | return element.innerHTML; 42 | } 43 | element.innerHTML = (value == null ? '' : value); 44 | }, 45 | value: function(generic, element, value){ 46 | var inputType = element.type; 47 | 48 | if(element.nodeName === 'INPUT' && inputType === 'date'){ 49 | if(arguments.length === 2){ 50 | return element.value ? new Date(element.value.replace(/-/g,'/').replace('T',' ')) : null; 51 | } 52 | 53 | value = value != null ? new Date(value) : null; 54 | 55 | if(!value || isNaN(value)){ 56 | element.value = null; 57 | }else{ 58 | element.value = [ 59 | value.getFullYear(), 60 | ('0' + (value.getMonth() + 1)).slice(-2), 61 | ('0' + value.getDate()).slice(-2) 62 | ].join('-'); 63 | } 64 | return; 65 | } 66 | 67 | if(arguments.length === 2){ 68 | return element.value; 69 | } 70 | if(value === undefined){ 71 | value = null; 72 | } 73 | 74 | if(element.nodeName === 'PROGRESS'){ 75 | value = parseFloat(value) || 0; 76 | } 77 | 78 | setify(element, value); 79 | }, 80 | max: function(generic, element, value) { 81 | if(arguments.length === 2){ 82 | return element.value; 83 | } 84 | 85 | if(element.nodeName === 'PROGRESS'){ 86 | value = parseFloat(value) || 0; 87 | } 88 | 89 | element.max = value; 90 | }, 91 | style: function(generic, element, value){ 92 | if(arguments.length === 2){ 93 | return element.style; 94 | } 95 | 96 | if(typeof value === 'string'){ 97 | element.style = value; 98 | return; 99 | } 100 | 101 | for(var key in value){ 102 | element.style[key] = value[key]; 103 | } 104 | }, 105 | type: function(generic, element, value){ 106 | if(arguments.length === 2){ 107 | return element.type; 108 | } 109 | element.setAttribute('type', value); 110 | } 111 | }; -------------------------------------------------------------------------------- /firmer.js: -------------------------------------------------------------------------------- 1 | // Is the entity firmer than the new firmness 2 | module.exports = function(entity, firm){ 3 | if(firm != null && (entity._firm === undefined || firm < entity._firm)){ 4 | return true; 5 | } 6 | }; -------------------------------------------------------------------------------- /genericComponent.js: -------------------------------------------------------------------------------- 1 | var containerComponent = require('./containerComponent'), 2 | schedule = require('./schedule'), 3 | fancyProps = require('./fancyProps'), 4 | matchDomHandlerName = /^((?:el\.)?)([^. ]+)(?:\.(capture))?$/, 5 | GENERIC = '_generic'; 6 | 7 | function createProperties(fastn, component, settings){ 8 | for(var key in settings){ 9 | var setting = settings[key]; 10 | 11 | if(typeof setting === 'function' && !fastn.isProperty(setting) && !fastn.isBinding(setting)){ 12 | continue; 13 | } 14 | 15 | component.addDomProperty(key); 16 | } 17 | } 18 | 19 | function trackKeyEvents(component, element, event){ 20 | if('_lastStates' in component && 'charCode' in event){ 21 | component._lastStates.unshift(element.value); 22 | component._lastStates.pop(); 23 | } 24 | } 25 | 26 | function addDomHandler(component, element, handlerName, eventName, capture){ 27 | var eventParts = handlerName.split('.'); 28 | 29 | if(eventParts[0] === 'on'){ 30 | eventParts.shift(); 31 | } 32 | 33 | var handler = function(event){ 34 | trackKeyEvents(component, element, event); 35 | component.emit(handlerName, event, component.scope()); 36 | }; 37 | 38 | element.addEventListener(eventName, handler, capture); 39 | 40 | component.on('destroy', function(){ 41 | element.removeEventListener(eventName, handler, capture); 42 | }); 43 | } 44 | 45 | function addDomHandlers(component, element, eventNames){ 46 | var events = eventNames.split(' '); 47 | 48 | for(var i = 0; i < events.length; i++){ 49 | var eventName = events[i], 50 | match = eventName.match(matchDomHandlerName); 51 | 52 | if(!match){ 53 | continue; 54 | } 55 | 56 | if(match[1] || 'on' + match[2] in element){ 57 | addDomHandler(component, element, eventNames, match[2], match[3]); 58 | } 59 | } 60 | } 61 | 62 | function addAutoHandler(component, element, key, settings){ 63 | if(!settings[key]){ 64 | return; 65 | } 66 | 67 | var eventName = key.slice(2); 68 | var handler = settings[key]; 69 | delete settings[key]; 70 | 71 | if(typeof handler === 'function'){ 72 | var innerHandler = handler; 73 | handler = function(event){ 74 | trackKeyEvents(component, element, event); 75 | innerHandler.call(component, event, component.scope()); 76 | }; 77 | } 78 | 79 | if (typeof handler === 'string') { 80 | var autoEvent = handler.split(':'); 81 | 82 | handler = function(event){ 83 | var fancyProp = fancyProps[autoEvent[1]], 84 | value = fancyProp ? fancyProp(component, element) : element[autoEvent[1]]; 85 | 86 | trackKeyEvents(component, element, event); 87 | 88 | component[autoEvent[0]](value); 89 | }; 90 | } 91 | 92 | element.addEventListener(eventName, handler); 93 | 94 | component.on('destroy', function(){ 95 | element.removeEventListener(eventName, handler); 96 | }); 97 | } 98 | 99 | function addDomProperty(fastn, key, property){ 100 | var component = this, 101 | timeout; 102 | 103 | property = property || component[key] || fastn.property(); 104 | component.setProperty(key, property); 105 | 106 | function update(){ 107 | var element = component.getPropertyElement(key), 108 | value = property(); 109 | 110 | if(!element || component.destroyed()){ 111 | return; 112 | } 113 | 114 | if( 115 | key === 'value' && 116 | component._lastStates && 117 | ~component._lastStates.indexOf(value) 118 | ){ 119 | clearTimeout(timeout); 120 | timeout = setTimeout(update, 50); 121 | return; 122 | } 123 | 124 | var isProperty = key in element.constructor.prototype || !('getAttribute' in element), 125 | fancyProp = component._fancyProps && component._fancyProps(key) || fancyProps[key], 126 | previous = fancyProp ? fancyProp(component, element) : isProperty ? element[key] : element.getAttribute(key); 127 | 128 | if(!fancyProp && !isProperty && value === null){ 129 | value = ''; 130 | } 131 | 132 | if(value !== previous){ 133 | if(fancyProp){ 134 | fancyProp(component, element, value); 135 | return; 136 | } 137 | 138 | if(isProperty){ 139 | element[key] = value; 140 | return; 141 | } 142 | 143 | if(typeof value !== 'function' && typeof value !== 'object'){ 144 | if(value === undefined) { 145 | element.removeAttribute(key); 146 | } else { 147 | element.setAttribute(key, value); 148 | } 149 | } 150 | } 151 | } 152 | 153 | property.updater(update); 154 | } 155 | 156 | function onRender(){ 157 | var component = this, 158 | element; 159 | 160 | for(var key in component._settings){ 161 | element = component.getEventElement(key); 162 | if(key.slice(0,2) === 'on' && key in element){ 163 | addAutoHandler(component, element, key, component._settings); 164 | } 165 | } 166 | 167 | for(var eventKey in component._events){ 168 | element = component.getEventElement(key); 169 | addDomHandlers(component, element, eventKey); 170 | } 171 | } 172 | 173 | function render(){ 174 | this.element = this.element || this.createElement(this._settings.tagName || this._tagName); 175 | 176 | if('value' in this.element){ 177 | this._lastStates = new Array(2); 178 | } 179 | 180 | this.emit('render'); 181 | 182 | return this; 183 | }; 184 | 185 | function genericComponent(fastn, component, type, settings, children){ 186 | if(component.is(type)){ 187 | return component; 188 | } 189 | 190 | if(global.Element && type instanceof global.Element){ 191 | component.element = type; 192 | type = component.element.tagName; 193 | } 194 | 195 | if(global.Node && type instanceof global.Node){ 196 | return fastn('text', { text: type }, type.textContent); 197 | } 198 | 199 | if(typeof type !== 'string'){ 200 | return; 201 | } 202 | 203 | if(type === GENERIC){ 204 | component._tagName = component._tagName || 'div'; 205 | }else{ 206 | component._tagName = type; 207 | } 208 | 209 | if(component.is(GENERIC)){ 210 | return component; 211 | } 212 | 213 | component.extend('_container', settings, children); 214 | 215 | component.addDomProperty = addDomProperty.bind(component, fastn); 216 | component.getEventElement = component.getContainerElement; 217 | component.getPropertyElement = component.getContainerElement; 218 | component.updateProperty = genericComponent.updateProperty; 219 | component.createElement = genericComponent.createElement; 220 | 221 | createProperties(fastn, component, settings); 222 | 223 | component.render = render.bind(component); 224 | 225 | component.on('render', onRender); 226 | 227 | return component; 228 | } 229 | 230 | genericComponent.updateProperty = function(component, property, update){ 231 | if(typeof document !== 'undefined' && document.contains(component.element)){ 232 | schedule(property, update); 233 | }else{ 234 | update(); 235 | } 236 | }; 237 | 238 | genericComponent.createElement = function(tagName){ 239 | if(tagName instanceof Node){ 240 | return tagName; 241 | } 242 | return document.createElement(tagName); 243 | }; 244 | 245 | module.exports = genericComponent; -------------------------------------------------------------------------------- /images/fastn-sml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KoryNunn/fastn/665896739c8d0432ea1c378d5f255bd76207273c/images/fastn-sml.png -------------------------------------------------------------------------------- /images/fastn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KoryNunn/fastn/665896739c8d0432ea1c378d5f255bd76207273c/images/fastn.png -------------------------------------------------------------------------------- /index.browser.js: -------------------------------------------------------------------------------- 1 | console.error("Error: Cannot find module 'enti' from '/home/kory/dev/fastn'"); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var createProperty = require('./property'), 2 | createBinding = require('./binding'), 3 | BaseComponent = require('./baseComponent'), 4 | Enti = require('enti'), 5 | objectAssign = require('object-assign'), 6 | is = require('./is'); 7 | 8 | function inflateProperties(component, settings){ 9 | for(var key in settings){ 10 | var setting = settings[key], 11 | property = component[key]; 12 | 13 | if(is.property(settings[key])){ 14 | 15 | if(is.property(property)){ 16 | property.destroy(); 17 | } 18 | 19 | setting.addTo(component, key); 20 | 21 | }else if(is.property(property)){ 22 | 23 | if(is.binding(setting)){ 24 | property.binding(setting); 25 | }else{ 26 | property(setting); 27 | } 28 | 29 | property.addTo(component, key); 30 | } 31 | } 32 | } 33 | 34 | function validateExpectedComponents(components, componentName, expectedComponents){ 35 | expectedComponents = expectedComponents.filter(function(componentName){ 36 | return !(componentName in components); 37 | }); 38 | 39 | if(expectedComponents.length){ 40 | console.warn([ 41 | 'fastn("' + componentName + '") uses some components that have not been registered with fastn', 42 | 'Expected component constructors: ' + expectedComponents.join(', ') 43 | ].join('\n\n')); 44 | } 45 | } 46 | 47 | module.exports = function(components, debug){ 48 | 49 | if(!components || typeof components !== 'object'){ 50 | throw new Error('fastn must be initialised with a components object'); 51 | } 52 | 53 | components._container = components._container || require('./containerComponent'); 54 | 55 | function fastn(type){ 56 | var args = Array.prototype.slice.call(arguments); 57 | 58 | var settings = args[1], 59 | childrenIndex = 2, 60 | settingsChild = fastn.toComponent(args[1]); 61 | 62 | if(Array.isArray(args[1]) || settingsChild || !args[1]){ 63 | if(args.length > 1){ 64 | args[1] = settingsChild || args[1]; 65 | } 66 | childrenIndex--; 67 | settings = null; 68 | } 69 | 70 | settings = objectAssign({}, settings || {}); 71 | 72 | var types = typeof type === 'string' ? type.split(':') : Array.isArray(type) ? type : [type], 73 | baseType, 74 | children = args.slice(childrenIndex), 75 | component = fastn.base(type, settings, children); 76 | 77 | while(component && (baseType = types.shift())){ 78 | component = component.extend(baseType, settings, children); 79 | } 80 | 81 | if(!component){ 82 | // Type was not a component. 83 | return; 84 | } 85 | 86 | component._properties = {}; 87 | 88 | inflateProperties(component, settings); 89 | 90 | return component; 91 | } 92 | 93 | fastn.toComponent = function(component){ 94 | if(component == null || Array.isArray(component)){ 95 | return; 96 | } 97 | 98 | if(is.component(component)){ 99 | return component; 100 | } 101 | 102 | if(typeof component !== 'object' || component instanceof Date){ 103 | return fastn('text', { text: component }, component); 104 | } 105 | 106 | return fastn(component) 107 | }; 108 | 109 | fastn.debug = debug; 110 | fastn.property = createProperty.bind(fastn); 111 | fastn.binding = createBinding(fastn); 112 | fastn.isComponent = is.component; 113 | fastn.isBinding = is.binding; 114 | fastn.isDefaultBinding = is.defaultBinding; 115 | fastn.isBindingObject = is.bindingObject; 116 | fastn.isProperty = is.property; 117 | fastn.components = components; 118 | fastn.Model = Enti; 119 | fastn.isModel = Enti.isEnti.bind(Enti); 120 | 121 | fastn.base = function(type, settings, children){ 122 | return new BaseComponent(fastn, type, settings, children); 123 | }; 124 | 125 | for(var key in components){ 126 | var componentConstructor = components[key]; 127 | 128 | if(componentConstructor.expectedComponents){ 129 | validateExpectedComponents(components, key, componentConstructor.expectedComponents); 130 | } 131 | } 132 | 133 | return fastn; 134 | }; 135 | -------------------------------------------------------------------------------- /is.js: -------------------------------------------------------------------------------- 1 | var FUNCTION = 'function', 2 | OBJECT = 'object', 3 | FASTNBINDING = '_fastn_binding', 4 | FASTNPROPERTY = '_fastn_property', 5 | FASTNCOMPONENT = '_fastn_component', 6 | DEFAULTBINDING = '_default_binding'; 7 | 8 | function isComponent(thing){ 9 | return thing && typeof thing === OBJECT && FASTNCOMPONENT in thing; 10 | } 11 | 12 | function isBindingObject(thing){ 13 | return thing && typeof thing === OBJECT && FASTNBINDING in thing; 14 | } 15 | 16 | function isBinding(thing){ 17 | return typeof thing === FUNCTION && FASTNBINDING in thing; 18 | } 19 | 20 | function isProperty(thing){ 21 | return typeof thing === FUNCTION && FASTNPROPERTY in thing; 22 | } 23 | 24 | function isDefaultBinding(thing){ 25 | return typeof thing === FUNCTION && FASTNBINDING in thing && DEFAULTBINDING in thing; 26 | } 27 | 28 | module.exports = { 29 | component: isComponent, 30 | bindingObject: isBindingObject, 31 | binding: isBinding, 32 | defaultBinding: isDefaultBinding, 33 | property: isProperty 34 | }; -------------------------------------------------------------------------------- /listComponent.js: -------------------------------------------------------------------------------- 1 | var MultiMap = require('multimap'), 2 | merge = require('flat-merge'); 3 | 4 | var requestIdleCallback = global.requestIdleCallback || global.requestAnimationFrame || global.setTimeout; 5 | 6 | MultiMap.Map = Map; 7 | 8 | function each(value, fn){ 9 | if(!value || typeof value !== 'object'){ 10 | return; 11 | } 12 | 13 | if(Array.isArray(value)){ 14 | for(var i = 0; i < value.length; i++){ 15 | fn(value[i], i) 16 | } 17 | }else{ 18 | for(var key in value){ 19 | fn(value[key], key); 20 | } 21 | } 22 | } 23 | 24 | function keyFor(object, value){ 25 | if(!object || typeof object !== 'object'){ 26 | return false; 27 | } 28 | 29 | if(Array.isArray(object)){ 30 | var index = object.indexOf(value); 31 | return index >=0 ? index : false; 32 | } 33 | 34 | for(var key in object){ 35 | if(object[key] === value){ 36 | return key; 37 | } 38 | } 39 | 40 | return false; 41 | } 42 | 43 | module.exports = function(fastn, component, type, settings, children){ 44 | 45 | if(fastn.components._generic){ 46 | component.extend('_generic', settings, children); 47 | }else{ 48 | component.extend('_container', settings, children); 49 | } 50 | 51 | if(!('template' in settings)){ 52 | console.warn('No "template" function was set for this templater component'); 53 | } 54 | 55 | var itemsMap = new MultiMap(), 56 | dataMap = new WeakMap(), 57 | lastTemplate, 58 | existingItem = {}; 59 | 60 | var insertQueue = []; 61 | var inserting; 62 | 63 | function updateOrCreateChild(template, item, key){ 64 | var child, 65 | existing; 66 | 67 | if(Array.isArray(item) && item[0] === existingItem){ 68 | existing = true; 69 | child = item[2]; 70 | item = item[1]; 71 | } 72 | 73 | var childModel; 74 | 75 | if(!existing){ 76 | childModel = new fastn.Model({ 77 | item: item, 78 | key: key 79 | }); 80 | 81 | child = fastn.toComponent(template(childModel, component.scope())); 82 | if(!child){ 83 | child = fastn('template'); 84 | } 85 | child._listItem = item; 86 | child._templated = true; 87 | 88 | dataMap.set(child, childModel); 89 | itemsMap.set(item, child); 90 | }else{ 91 | childModel = dataMap.get(child); 92 | childModel.set('key', key); 93 | } 94 | 95 | if(fastn.isComponent(child) && component._settings.attachTemplates !== false){ 96 | child.attach(childModel, 2); 97 | } 98 | 99 | return child; 100 | } 101 | 102 | function insertNextItems(template, insertionFrameTime){ 103 | if(inserting){ 104 | return; 105 | } 106 | 107 | inserting = true; 108 | component.emit('insertionStart', insertQueue.length); 109 | 110 | insertQueue.sort(function(a, b){ 111 | return a[2] - b[2]; 112 | }); 113 | 114 | function insertNext(){ 115 | var startTime = Date.now(); 116 | 117 | while(insertQueue.length && Date.now() - startTime < insertionFrameTime) { 118 | var nextInsersion = insertQueue.shift(); 119 | var child = updateOrCreateChild(template, nextInsersion[0], nextInsersion[1]); 120 | component.insert(child, nextInsersion[2]); 121 | } 122 | 123 | if(!insertQueue.length || component.destroyed()){ 124 | inserting = false; 125 | if(!component.destroyed()){ 126 | component.emit('insertionComplete'); 127 | } 128 | return; 129 | } 130 | 131 | requestIdleCallback(insertNext); 132 | } 133 | 134 | insertNext(); 135 | } 136 | 137 | function updateItems(){ 138 | insertQueue = []; 139 | 140 | var value = component.items(), 141 | template = component.template(), 142 | emptyTemplate = component.emptyTemplate(), 143 | insertionFrameTime = component.insertionFrameTime() || Infinity, 144 | newTemplate = lastTemplate !== template; 145 | 146 | var currentItems = merge(template ? value : []); 147 | 148 | itemsMap.forEach(function(childComponent, item){ 149 | var currentKey = keyFor(currentItems, item); 150 | 151 | if(!newTemplate && currentKey !== false){ 152 | currentItems[currentKey] = [existingItem, item, childComponent]; 153 | }else{ 154 | removeComponent(childComponent); 155 | itemsMap.delete(item, childComponent); 156 | } 157 | }); 158 | 159 | var index = 0; 160 | var templateIndex = 0; 161 | 162 | function updateItem(item, key){ 163 | while(index < component._children.length && !component._children[index]._templated){ 164 | index++; 165 | } 166 | 167 | insertQueue.push([item, key, index + templateIndex]); 168 | templateIndex++; 169 | } 170 | 171 | each(currentItems, updateItem); 172 | 173 | template && insertNextItems(template, insertionFrameTime); 174 | 175 | lastTemplate = template; 176 | 177 | if(templateIndex === 0 && emptyTemplate){ 178 | var child = fastn.toComponent(emptyTemplate(component.scope())); 179 | if(!child){ 180 | child = fastn('template'); 181 | } 182 | child._templated = true; 183 | 184 | itemsMap.set({}, child); 185 | 186 | component.insert(child); 187 | } 188 | } 189 | 190 | function removeComponent(childComponent){ 191 | component.remove(childComponent); 192 | childComponent.destroy(); 193 | } 194 | 195 | component.setProperty('insertionFrameTime'); 196 | 197 | component.setProperty('items', 198 | fastn.property([], settings.itemChanges || 'type keys shallowStructure') 199 | .on('change', updateItems) 200 | ); 201 | 202 | component.setProperty('template', 203 | fastn.property().on('change', updateItems) 204 | ); 205 | 206 | component.setProperty('emptyTemplate', 207 | fastn.property().on('change', updateItems) 208 | ); 209 | 210 | return component; 211 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastn", 3 | "version": "2.14.5", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test", 8 | "watch": "watchify test/index.js -o test/index.browser.js -d" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/KoryNunn/fastn.git" 15 | }, 16 | "dependencies": { 17 | "classist": "^1.1.1", 18 | "enti": "^6.1.3", 19 | "flat-merge": "^1.0.0", 20 | "function-emitter": "^1.0.0", 21 | "multimap": "^1.0.2", 22 | "object-assign": "^4.1.1", 23 | "same-value": "^1.0.2", 24 | "setify": "^1.0.3", 25 | "setprototypeof": "^1.1.0", 26 | "what-changed": "^2.3.0" 27 | }, 28 | "devDependencies": { 29 | "browserify": "^14.5.0", 30 | "console-watch": "^1.0.2", 31 | "crel": "^4.0.1", 32 | "dom-lightning": "^1.0.2", 33 | "dom-lite": "^0.5.1", 34 | "tape": "^5.0.1", 35 | "tape-run": "^6.0.1", 36 | "watchify": "^3.11.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /property.js: -------------------------------------------------------------------------------- 1 | var WhatChanged = require('what-changed'), 2 | same = require('same-value'), 3 | firmer = require('./firmer'), 4 | functionEmitter = require('function-emitter'), 5 | setPrototypeOf = require('setprototypeof'); 6 | 7 | var propertyProto = Object.create(functionEmitter); 8 | 9 | propertyProto._fastn_property = true; 10 | propertyProto._firm = 1; 11 | 12 | function propertyTemplate(value){ 13 | if(!arguments.length){ 14 | return this.observable && typeof this.observable === 'function' && this.observable() || this.property._value; 15 | } 16 | 17 | if(!this.destroyed){ 18 | if(this.observable){ 19 | typeof this.observable === 'function' && this.observable(value); 20 | return this.property; 21 | } 22 | 23 | this.valueUpdate(value); 24 | } 25 | 26 | return this.property; 27 | } 28 | 29 | function changeChecker(current, changes){ 30 | if(changes){ 31 | var changes = new WhatChanged(current, changes); 32 | 33 | return function(value){ 34 | return changes.update(value).any; 35 | }; 36 | }else{ 37 | var lastValue = current; 38 | return function(newValue){ 39 | if(!same(lastValue, newValue)){ 40 | lastValue = newValue; 41 | return true; 42 | } 43 | }; 44 | } 45 | } 46 | 47 | function propertyBinding(newBinding){ 48 | if(!arguments.length){ 49 | return this.observable; 50 | } 51 | 52 | if(typeof newBinding === 'string' || typeof newBinding === 'number'){ 53 | newBinding = this.fastn.binding(newBinding); 54 | } 55 | 56 | if(newBinding === this.observable){ 57 | return this.property; 58 | } 59 | 60 | if(this.observable){ 61 | this.observable.removeListener('change', this.valueUpdate); 62 | } 63 | 64 | this.observable = newBinding; 65 | 66 | if(this.model){ 67 | this.property.attach(this.model, this.property._firm); 68 | } 69 | 70 | this.observable.on('change', this.valueUpdate); 71 | if(typeof this.observable === 'function'){ 72 | this.valueUpdate(this.observable()); 73 | } else { 74 | this.valueUpdate(undefined); 75 | } 76 | 77 | return this.property; 78 | } 79 | 80 | function attachProperty(object, firm){ 81 | if(firmer(this.property, firm)){ 82 | return this.property; 83 | } 84 | 85 | this.property._firm = firm; 86 | 87 | if(!(object instanceof Object)){ 88 | object = {}; 89 | } 90 | 91 | if(this.observable){ 92 | this.model = object; 93 | this.observable.attach && this.observable.attach(object, 1); 94 | } 95 | 96 | if(this.property._events && 'attach' in this.property._events){ 97 | this.property.emit('attach', object, 1); 98 | } 99 | 100 | return this.property; 101 | }; 102 | 103 | function detachProperty(firm){ 104 | if(firmer(this.property, firm)){ 105 | return this.property; 106 | } 107 | 108 | if(this.observable){ 109 | this.observable.removeListener('change', this.valueUpdate); 110 | this.observable.detach && this.observable.detach(1); 111 | this.model = null; 112 | } 113 | 114 | if(this.property._events && 'detach' in this.property._events){ 115 | this.property.emit('detach', 1); 116 | } 117 | 118 | return this.property; 119 | }; 120 | 121 | function updateProperty(){ 122 | if(!this.destroyed){ 123 | 124 | if(this.property._update){ 125 | this.property._update(this.property._value, this.property); 126 | } 127 | 128 | this.property.emit('update', this.property._value); 129 | } 130 | return this.property; 131 | }; 132 | 133 | function propertyUpdater(fn){ 134 | if(!arguments.length){ 135 | return this.property._update; 136 | } 137 | this.property._update = fn; 138 | return this.property; 139 | }; 140 | 141 | function destroyProperty(){ 142 | if(!this.destroyed){ 143 | this.destroyed = true; 144 | 145 | this.property 146 | .removeAllListeners('change') 147 | .removeAllListeners('update') 148 | .removeAllListeners('attach'); 149 | 150 | this.property.emit('destroy'); 151 | this.property.detach(); 152 | if(this.observable){ 153 | this.observable.destroy && this.observable.destroy(true); 154 | } 155 | } 156 | return this.property; 157 | }; 158 | 159 | function propertyDestroyed(){ 160 | return this.destroyed; 161 | }; 162 | 163 | function addPropertyTo(component, key){ 164 | component.setProperty(key, this.property); 165 | 166 | return this.property; 167 | }; 168 | 169 | function createProperty(currentValue, changes, updater){ 170 | if(typeof changes === 'function'){ 171 | updater = changes; 172 | changes = null; 173 | } 174 | 175 | var propertyScope = { 176 | fastn: this, 177 | hasChanged: changeChecker(currentValue, changes) 178 | }, 179 | property = propertyTemplate.bind(propertyScope); 180 | 181 | propertyScope.valueUpdate = function(value){ 182 | property._value = value; 183 | if(!propertyScope.hasChanged(value)){ 184 | return; 185 | } 186 | property.emit('change', property._value); 187 | property.update(); 188 | }; 189 | 190 | var property = propertyScope.property = propertyTemplate.bind(propertyScope); 191 | 192 | property._value = currentValue; 193 | property._update = updater; 194 | 195 | setPrototypeOf(property, propertyProto); 196 | 197 | property.binding = propertyBinding.bind(propertyScope); 198 | property.attach = attachProperty.bind(propertyScope); 199 | property.detach = detachProperty.bind(propertyScope); 200 | property.update = updateProperty.bind(propertyScope); 201 | property.updater = propertyUpdater.bind(propertyScope); 202 | property.destroy = destroyProperty.bind(propertyScope); 203 | property.destroyed = propertyDestroyed.bind(propertyScope); 204 | property.addTo = addPropertyTo.bind(propertyScope); 205 | 206 | return property; 207 | }; 208 | 209 | module.exports = createProperty; -------------------------------------------------------------------------------- /schedule.js: -------------------------------------------------------------------------------- 1 | var todo = [], 2 | todoKeys = [], 3 | scheduled, 4 | updates = 0; 5 | 6 | function run(){ 7 | var startTime = Date.now(); 8 | 9 | while(todo.length && Date.now() - startTime < 16){ 10 | todoKeys.shift(); 11 | todo.shift()(); 12 | } 13 | 14 | if(todo.length){ 15 | requestAnimationFrame(run); 16 | }else{ 17 | scheduled = false; 18 | } 19 | } 20 | 21 | function schedule(key, fn){ 22 | if(~todoKeys.indexOf(key)){ 23 | return; 24 | } 25 | 26 | todo.push(fn); 27 | todoKeys.push(key); 28 | 29 | if(!scheduled){ 30 | scheduled = true; 31 | requestAnimationFrame(run); 32 | } 33 | } 34 | 35 | module.exports = schedule; -------------------------------------------------------------------------------- /templaterComponent.js: -------------------------------------------------------------------------------- 1 | module.exports = function(fastn, component, type, settings, children){ 2 | var itemModel = new fastn.Model({}); 3 | 4 | if(!('template' in settings)){ 5 | console.warn('No "template" function was set for this templater component'); 6 | } 7 | 8 | function replaceElement(element){ 9 | if(component.element && component.element.parentNode){ 10 | component.element.parentNode.replaceChild(element, component.element); 11 | } 12 | component.element = element; 13 | } 14 | 15 | function update(){ 16 | 17 | var value = component.data(), 18 | template = component.template(); 19 | 20 | itemModel.set('item', value); 21 | 22 | var newComponent; 23 | 24 | if(template){ 25 | newComponent = fastn.toComponent(template(itemModel, component.scope(), component._currentComponent)); 26 | } 27 | 28 | if(component._currentComponent && component._currentComponent !== newComponent){ 29 | if(fastn.isComponent(component._currentComponent)){ 30 | component._currentComponent.destroy(); 31 | } 32 | } 33 | 34 | component._currentComponent = newComponent; 35 | 36 | if(!newComponent){ 37 | replaceElement(component.emptyElement); 38 | return; 39 | } 40 | 41 | if(fastn.isComponent(newComponent)){ 42 | if(component._settings.attachTemplates !== false){ 43 | newComponent.attach(itemModel, 2); 44 | }else{ 45 | newComponent.attach(component.scope(), 1); 46 | } 47 | 48 | if(component.element && component.element !== newComponent.element){ 49 | if(newComponent.element == null){ 50 | newComponent.render(); 51 | } 52 | replaceElement(component._currentComponent.element); 53 | } 54 | } 55 | } 56 | 57 | component.render = function(){ 58 | var element; 59 | component.emptyElement = document.createTextNode(''); 60 | if(component._currentComponent){ 61 | component._currentComponent.render(); 62 | element = component._currentComponent.element; 63 | } 64 | component.element = element || component.emptyElement; 65 | component.emit('render'); 66 | return component; 67 | }; 68 | 69 | component.setProperty('data', 70 | fastn.property(undefined, settings.dataChanges || 'value structure') 71 | .on('change', update) 72 | ); 73 | 74 | component.setProperty('template', 75 | fastn.property(undefined, 'value reference') 76 | .on('change', update) 77 | ); 78 | 79 | component.on('destroy', function(){ 80 | if(fastn.isComponent(component._currentComponent)){ 81 | component._currentComponent.destroy(); 82 | } 83 | }); 84 | 85 | component.on('attach', function(data){ 86 | if(fastn.isComponent(component._currentComponent)){ 87 | component._currentComponent.attach(component.scope(), 1); 88 | } 89 | }); 90 | 91 | return component; 92 | }; -------------------------------------------------------------------------------- /test/attach.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | Enti = require('enti'), 3 | createFastn = require('./createFastn'); 4 | 5 | test('manual attach', function(t){ 6 | 7 | t.plan(3); 8 | 9 | var fastn = createFastn(); 10 | 11 | var child, 12 | parent = fastn('div', 13 | child = fastn('span') 14 | ); 15 | 16 | parent.attach({ 17 | foo:'bar' 18 | }); 19 | 20 | t.deepEqual(parent.scope().get('.'), { 21 | foo:'bar' 22 | }); 23 | 24 | t.deepEqual(child.scope().get('.'), { 25 | foo:'bar' 26 | }); 27 | 28 | t.equal(parent.scope().get('.'), child.scope().get('.')); 29 | 30 | }); 31 | 32 | test('weak attach attempt', function(t){ 33 | 34 | t.plan(3); 35 | 36 | var fastn = createFastn(); 37 | 38 | var child, 39 | parent = fastn('div', 40 | child = fastn('span') 41 | ); 42 | 43 | parent.attach({ 44 | foo:'bar' 45 | }); 46 | 47 | child.attach({ 48 | baz: 'inga' 49 | }, 0); 50 | 51 | t.deepEqual(parent.scope().get('.'), { 52 | foo:'bar' 53 | }); 54 | 55 | t.deepEqual(child.scope().get('.'), { 56 | foo:'bar' 57 | }); 58 | 59 | t.equal(parent.scope().get('.'), child.scope().get('.')); 60 | }); 61 | 62 | test('firmer attach attempt', function(t){ 63 | 64 | t.plan(3); 65 | 66 | var fastn = createFastn(); 67 | 68 | var child, 69 | parent = fastn('div', 70 | child = fastn('span') 71 | ); 72 | 73 | parent.attach({ 74 | foo:'bar' 75 | }); 76 | 77 | child.attach({ 78 | baz: 'inga' 79 | }, 1); 80 | 81 | t.deepEqual(parent.scope().get('.'), { 82 | foo:'bar' 83 | }); 84 | 85 | t.deepEqual(child.scope().get('.'), { 86 | baz:'inga' 87 | }); 88 | 89 | t.notEqual(parent.scope().get('.'), child.scope().get('.')); 90 | }); 91 | 92 | test('firmest attach', function(t){ 93 | 94 | t.plan(3); 95 | 96 | var fastn = createFastn(); 97 | 98 | var child, 99 | parent = fastn('div', 100 | child = fastn('span') 101 | ); 102 | 103 | parent.attach({ 104 | foo:'bar' 105 | }); 106 | 107 | child.attach({ 108 | baz: 'inga' 109 | }); 110 | 111 | t.deepEqual(parent.scope().get('.'), { 112 | foo:'bar' 113 | }); 114 | 115 | t.deepEqual(child.scope().get('.'), { 116 | baz:'inga' 117 | }); 118 | 119 | t.notEqual(parent.scope().get('.'), child.scope().get('.')); 120 | }); 121 | -------------------------------------------------------------------------------- /test/binding.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | createBinding = require('../index')({}).binding, 3 | Enti = require('enti'); 4 | 5 | test('invalid path', function(t){ 6 | t.plan(5); 7 | 8 | t.throws(function(){ 9 | createBinding(true); 10 | }); 11 | t.throws(function(){ 12 | createBinding({}); 13 | }); 14 | t.throws(function(){ 15 | createBinding(function(){}); 16 | }); 17 | t.throws(function(){ 18 | createBinding(null); 19 | }); 20 | t.throws(function(){ 21 | createBinding(undefined); 22 | }); 23 | }); 24 | 25 | test('simple binding initialisation', function(t){ 26 | t.plan(3); 27 | 28 | var binding = createBinding('foo'); 29 | 30 | var model = {}, 31 | enti = new Enti(model); 32 | 33 | t.equal(binding(), undefined); 34 | 35 | enti.set('foo', 'bar'); 36 | 37 | t.equal(binding(), undefined); 38 | 39 | binding.attach(model); 40 | 41 | t.equal(binding(), 'bar'); 42 | }); 43 | 44 | test('initial attach doesnt cause emit', function(t){ 45 | t.plan(1); 46 | 47 | var binding = createBinding('foo'); 48 | 49 | binding.on('change', () => t.pass('Recieved change')); 50 | 51 | binding.attach(); 52 | binding.attach({}); 53 | }); 54 | 55 | test('simple binding set', function(t){ 56 | t.plan(2); 57 | 58 | var binding = createBinding('foo'); 59 | 60 | binding.attach({}); 61 | 62 | t.equal(binding(), undefined); 63 | 64 | binding('bazinga'); 65 | 66 | t.equal(binding(), 'bazinga'); 67 | }); 68 | 69 | test('simple binding event', function(t){ 70 | t.plan(3); 71 | 72 | var binding = createBinding('foo'); 73 | 74 | var model = {}, 75 | enti = new Enti(model); 76 | 77 | binding.attach(model); 78 | 79 | binding.once('change', function(value){ 80 | t.equal(value, 'bar'); 81 | t.equal(binding(), 'bar'); 82 | }); 83 | 84 | enti.set('foo', 'bar'); 85 | 86 | binding.once('detach', function(){ 87 | t.equal(binding(), undefined); 88 | }); 89 | 90 | binding.detach(); 91 | 92 | enti.set('foo', 'baz'); 93 | }); 94 | 95 | test('no model', function(t){ 96 | t.plan(3); 97 | 98 | var binding = createBinding('foo'); 99 | 100 | t.equal(binding(), undefined); 101 | 102 | binding.on('change', function(value){ 103 | t.equal(value, 'bar'); 104 | console.log(value) 105 | }); 106 | 107 | binding('bar'); 108 | console.log(binding()) 109 | 110 | t.equal(binding(), 'bar'); 111 | }); 112 | 113 | test('drill get', function(t){ 114 | t.plan(2); 115 | 116 | var data = { 117 | foo: { 118 | bar: 123 119 | } 120 | }, 121 | model = new Enti(data), 122 | binding = createBinding('foo.bar'); 123 | 124 | binding.attach(data); 125 | 126 | t.equal(binding(), 123); 127 | 128 | model.set('foo', { 129 | bar: 456 130 | }); 131 | 132 | t.equal(binding(), 456); 133 | }); 134 | 135 | test('drill change', function(t){ 136 | t.plan(1); 137 | 138 | var data = { 139 | foo: { 140 | bar: 123 141 | } 142 | }, 143 | model = new Enti(data), 144 | binding = createBinding('foo.bar'); 145 | 146 | binding.attach(data); 147 | 148 | binding.on('change', function(){ 149 | t.pass('target changed'); 150 | }); 151 | 152 | model.set('foo', { 153 | bar: 456 154 | }); 155 | }); 156 | 157 | test('drill attach', function(t){ 158 | t.plan(2); 159 | 160 | var data = { 161 | foo: { 162 | bar: 123 163 | } 164 | }, 165 | model = new Enti(data), 166 | binding = createBinding('foo.bar'); 167 | 168 | 169 | binding.once('change', function(value){ 170 | t.equal(value, 123); 171 | }); 172 | 173 | binding.attach(data); 174 | 175 | binding.once('change', function(value){ 176 | t.equal(value, 456); 177 | }); 178 | 179 | model.set('foo', { 180 | bar: 456 181 | }); 182 | }); 183 | 184 | test('drill set', function(t){ 185 | t.plan(1); 186 | 187 | var data = { 188 | foo: { 189 | bar: 123 190 | } 191 | }, 192 | model = new Enti(data), 193 | fooModel = new Enti(data.foo), 194 | binding = createBinding('foo.bar'); 195 | 196 | 197 | fooModel.on('bar', function(value){ 198 | t.equal(value, 456); 199 | }); 200 | 201 | binding.attach(data); 202 | 203 | binding(456); 204 | }); 205 | 206 | test('drill multiple', function(t){ 207 | t.plan(3); 208 | 209 | var data = { 210 | foo: { 211 | bar: 123 212 | } 213 | }, 214 | model = new Enti(data), 215 | fooModel = new Enti(data.foo), 216 | binding = createBinding('foo.bar'); 217 | 218 | 219 | fooModel.once('bar', function(value){ 220 | t.equal(value, 456); 221 | }); 222 | 223 | binding.attach(data); 224 | 225 | binding(456); 226 | 227 | binding.once('change', function(value){ 228 | t.equal(value, 789); 229 | }); 230 | 231 | fooModel.set('bar', 789); 232 | 233 | binding.once('change', function(value){ 234 | t.equal(value, 987); 235 | }); 236 | 237 | binding(987); 238 | }); 239 | 240 | test('fuse', function(t){ 241 | t.plan(2); 242 | 243 | var data = { 244 | foo: 1, 245 | bar: 2, 246 | baz: 3 247 | }, 248 | model = new Enti(data), 249 | binding = createBinding('foo', 'bar', 'baz', function(foo, bar, baz){ 250 | return foo + bar + baz; 251 | }); 252 | 253 | binding.attach(data); 254 | 255 | binding(2); 256 | 257 | binding.once('change', function(value){ 258 | t.equal(value, 7); 259 | }); 260 | 261 | model.set('bar', 3); 262 | 263 | binding.once('change', function(value){ 264 | t.equal(value, 3); 265 | }); 266 | 267 | binding(3); 268 | }); 269 | 270 | test('fuse attached to object with result', function(t){ 271 | t.plan(1); 272 | 273 | var data = { 274 | foo: 1, 275 | bar: 2, 276 | result: 'result' 277 | }, 278 | binding = createBinding('foo', 'bar', function(foo, bar){ 279 | return foo + bar; 280 | }); 281 | 282 | binding.on('change', value => t.equal(value, 3)); 283 | binding.attach(data); 284 | }); 285 | 286 | test('fuse firmness', function(t){ 287 | t.plan(1); 288 | 289 | var data1 = { 290 | foo: 1 291 | }, 292 | binding = createBinding('foo', function(foo){ 293 | return foo; 294 | }); 295 | 296 | binding.on('change', value => t.equal(value, 1)); 297 | 298 | binding.attach({ foo: 1 }); 299 | 300 | binding.attach({ foo: 2 }, 1); 301 | }); 302 | 303 | test('fuse destroy inner used', function(t){ 304 | t.plan(4); 305 | 306 | var data1 = { 307 | foo: 1 308 | }, 309 | data2 = { 310 | bar: 1 311 | }, 312 | innerBinding = createBinding('foo').attach(data1), 313 | binding = createBinding(innerBinding, 'bar', function(foo, bar){ 314 | return foo + bar; 315 | }); 316 | 317 | // Add a listener to the inner binding 318 | // This should prevent destruction. 319 | innerBinding.on('change', function(){ 320 | t.pass('Inner binding changed'); 321 | }); 322 | 323 | binding.attach(data2); 324 | 325 | binding.once('change', function(value){ 326 | t.equal(value, 3); 327 | }); 328 | 329 | Enti.set(data1, 'foo', 2); 330 | 331 | binding.once('change', function(value){ 332 | t.fail('No event should occur since the binding is detached'); 333 | }); 334 | 335 | binding.destroy(); 336 | 337 | t.notOk(innerBinding.destroyed(), 'inner binding should not be destroyed'); 338 | 339 | Enti.set(data1, 'foo', 3); 340 | }); 341 | 342 | test('fuse set', function(t){ 343 | t.plan(1); 344 | 345 | var data = { 346 | a: 1, 347 | b: 2, 348 | c: 3 349 | }; 350 | 351 | var binding = createBinding('a', 'b', 'c', function(a, b, c){ 352 | return a + b + c; 353 | }, x => x).attach(data); 354 | 355 | binding(2); 356 | 357 | t.equal(data.a, 2); 358 | }); 359 | 360 | test('filter', function(t){ 361 | t.plan(2); 362 | 363 | var data = {}, 364 | model = new Enti(data), 365 | binding = createBinding('foo|*'); 366 | 367 | binding.attach(data); 368 | 369 | binding.on('change', function(value){ 370 | t.pass(); 371 | }); 372 | 373 | model.set('foo', []); 374 | 375 | Enti.set(data.foo, 0, {}); 376 | }); 377 | 378 | test('things', function(t){ 379 | t.plan(2); 380 | 381 | var data = {}, 382 | model = new Enti(data), 383 | binding = createBinding('foo|*.bar'); 384 | 385 | binding.attach(data); 386 | 387 | binding.on('change', function(value){ 388 | t.pass(); 389 | }); 390 | 391 | model.set('foo', [{}]); 392 | 393 | Enti.set(data.foo[0], 'bar', true); 394 | }); 395 | 396 | test('clone', function(t){ 397 | t.plan(4); 398 | 399 | var data1 = {foo:1}, 400 | data2 = {foo:2}, 401 | binding = createBinding('foo'); 402 | 403 | binding.attach(data1); 404 | 405 | t.equal(binding(), 1, 'Original binding has correct data'); 406 | 407 | var newBinding = binding.clone(); 408 | 409 | t.equal(newBinding(), undefined, 'New binding has no data'); 410 | 411 | newBinding.attach(data2); 412 | 413 | t.equal(newBinding(), 2, 'New binding has new data'); 414 | 415 | t.equal(binding(), 1, 'Original binding still has original data'); 416 | }); 417 | 418 | test('clone with attachment', function(t){ 419 | t.plan(2); 420 | 421 | var data1 = {foo:1}, 422 | binding = createBinding('foo'); 423 | 424 | binding.attach(data1); 425 | 426 | t.equal(binding(), 1, 'Original binding has correct data'); 427 | 428 | var newBinding = binding.clone(true); 429 | 430 | t.equal(newBinding(), 1, 'New binding has same data'); 431 | }); 432 | 433 | test('clone fuse', function(t){ 434 | t.plan(2); 435 | 436 | var data1 = {foo:1, bar:2}, 437 | binding = createBinding('foo', 'bar', function(foo, bar){ 438 | return foo + bar; 439 | }); 440 | 441 | binding.attach(data1); 442 | 443 | t.equal(binding(), 3, 'Original binding has correct data'); 444 | 445 | var newBinding = binding.clone(true); 446 | 447 | t.equal(newBinding(), 3, 'New binding has same data'); 448 | }); 449 | 450 | test('binding as a bindings target', function(t){ 451 | t.plan(1); 452 | 453 | var binding1 = createBinding('foo'), 454 | binding2 = createBinding('bar'); 455 | 456 | binding1(binding2); 457 | 458 | t.equal(binding1(), binding2, 'binding1 value correctly set to binding2'); 459 | }); 460 | 461 | test('binding as own target', function(t){ 462 | t.plan(1); 463 | 464 | var binding = createBinding('foo'); 465 | 466 | binding(binding); 467 | 468 | t.equal(binding(), binding, 'binding value correctly set to self'); 469 | }); 470 | 471 | test('value-only binding', function(t){ 472 | t.plan(1); 473 | 474 | var binding = createBinding(); 475 | 476 | binding('foo'); 477 | 478 | t.equal(binding(), 'foo', 'binding value correctly set to foo'); 479 | }); 480 | 481 | test('value-only binding cannot be attached', function(t){ 482 | t.plan(1); 483 | 484 | var binding = createBinding(); 485 | 486 | binding('foo'); 487 | 488 | binding.attach({ 489 | value: 'bar' 490 | }); 491 | 492 | t.equal(binding(), 'foo', 'binding value correctly set to foo'); 493 | }); 494 | 495 | test('destroy', function(t){ 496 | t.plan(1); 497 | 498 | var binding = createBinding().on('change', function(){ 499 | t.pass('binding changed'); 500 | }); 501 | 502 | binding('foo'); 503 | 504 | binding.destroy(); 505 | 506 | binding('bar'); 507 | }); 508 | 509 | test('soft destroy', function(t){ 510 | t.plan(2); 511 | 512 | var binding = createBinding().on('change', function(){ 513 | t.pass('binding changed'); 514 | }); 515 | 516 | binding('foo'); 517 | 518 | binding.destroy(true); 519 | 520 | binding('bar'); 521 | }); 522 | 523 | test('soft destroy 2', function(t){ 524 | t.plan(1); 525 | 526 | function changeHandler(){ 527 | t.pass('binding changed'); 528 | } 529 | 530 | var binding = createBinding().on('change', changeHandler); 531 | 532 | binding('foo'); 533 | 534 | binding.removeListener('change', changeHandler); 535 | binding.destroy(true); 536 | 537 | binding('bar'); 538 | }); 539 | 540 | test('model attach', function(t){ 541 | t.plan(2); 542 | 543 | var model = new Enti(); 544 | 545 | var binding = createBinding('a'); 546 | 547 | binding.attach(model); 548 | 549 | t.equal(binding(), undefined); 550 | 551 | model.attach({ 552 | a: 2 553 | }); 554 | 555 | t.equal(binding(), 2); 556 | 557 | }); 558 | 559 | test('from', function(t){ 560 | t.plan(3); 561 | 562 | var binding = createBinding(), 563 | value = 5; 564 | 565 | binding(10); 566 | 567 | var from1 = createBinding.from(binding); 568 | var from2 = createBinding.from(value); 569 | 570 | t.equal(from1(), 10); 571 | t.equal(from1, binding); 572 | t.equal(from2(), 5); 573 | 574 | }); 575 | 576 | test('binding as path', function(t){ 577 | t.plan(1); 578 | 579 | var binding1 = createBinding(), 580 | binding2 = createBinding(binding1); 581 | 582 | binding1(10); 583 | 584 | t.equal(binding2(), 10); 585 | 586 | }); 587 | 588 | test('detach memory usage', function(t){ 589 | t.plan(1); 590 | 591 | var data = { 592 | foo: null 593 | }; 594 | 595 | var runs = 0; 596 | 597 | function run(){ 598 | var bindings = []; 599 | for(var i = 0; i < 1000; i++){ 600 | bindings.push(createBinding('foo').attach(data)); 601 | } 602 | 603 | Enti.set(data, 'foo', runs); 604 | 605 | bindings.map(function(binding){ 606 | binding.detach(); 607 | }); 608 | 609 | setTimeout(function(){ 610 | if(runs++ < 10){ 611 | run(); 612 | } else { 613 | t.pass(); 614 | } 615 | }); 616 | } 617 | 618 | run(); 619 | }); 620 | -------------------------------------------------------------------------------- /test/changes.js: -------------------------------------------------------------------------------- 1 | const righto = require('righto'); 2 | 3 | const outputOnError = error => { error && console.log(error); }; 4 | 5 | function runTest (fn) { 6 | if (fn.constructor.name === 'GeneratorFunction') { 7 | return function () { 8 | const generator = righto.iterate(fn); 9 | const result = righto.apply(null, [generator].concat(Array.from(arguments))); 10 | result(outputOnError); 11 | }; 12 | } 13 | 14 | return fn; 15 | } 16 | 17 | function rightoTest (name, fn) { 18 | test(name, runTest(fn)); 19 | } 20 | 21 | module.exports = rightoTest; -------------------------------------------------------------------------------- /test/component.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | Enti = require('enti'), 3 | createFastn = require('./createFastn'); 4 | 5 | test('binding', function(t){ 6 | 7 | t.plan(2); 8 | 9 | var fastn = createFastn(); 10 | 11 | var data = { 12 | foo:{ 13 | bar:1 14 | } 15 | }, 16 | component = fastn('div'); 17 | 18 | component.attach(data); 19 | 20 | t.equal(component.scope().get('.'), data); 21 | 22 | component.binding('foo'); 23 | 24 | t.equal(component.scope().get('.'), data.foo); 25 | }); 26 | 27 | test('pre-created component', function(t){ 28 | 29 | t.plan(3); 30 | 31 | var fastn = createFastn({ 32 | custom: function(fastn, component, type, settings, children){ 33 | t.pass('Used custom constructor'); 34 | return component; 35 | } 36 | }); 37 | 38 | var data = { 39 | foo:{ 40 | bar:1 41 | } 42 | }, 43 | component = fastn('custom'); 44 | 45 | component.attach(data); 46 | 47 | t.equal(component.scope().get('.'), data); 48 | 49 | component.binding('foo'); 50 | 51 | t.equal(component.scope().get('.'), data.foo); 52 | }); 53 | 54 | test('auto extend component', function(t){ 55 | 56 | t.plan(6); 57 | 58 | var fastn = createFastn({ 59 | foo: function(fastn, component, type, settings, children){ 60 | t.pass('Used foo constructor'); 61 | return component; 62 | }, 63 | bar: function(fastn, component, type, settings, children){ 64 | t.pass('Used bar constructor'); 65 | return component; 66 | }, 67 | baz: function(fastn, component, type, settings, children){ 68 | t.pass('Used baz constructor'); 69 | return component; 70 | } 71 | }); 72 | 73 | var component = fastn('foo:bar:baz'); 74 | 75 | t.ok(component.is('foo'), 'componant is foo'); 76 | t.ok(component.is('bar'), 'componant is bar'); 77 | t.ok(component.is('baz'), 'componant is baz'); 78 | }); 79 | 80 | test('manual extend component', function(t){ 81 | 82 | t.plan(6); 83 | 84 | var fastn = createFastn({ 85 | foo: function(fastn, component, type, settings, children){ 86 | t.pass('Used foo constructor'); 87 | return component; 88 | }, 89 | bar: function(fastn, component, type, settings, children){ 90 | t.pass('Used bar constructor'); 91 | return component; 92 | }, 93 | baz: function(fastn, component, type, settings, children){ 94 | t.pass('Used baz constructor'); 95 | return component; 96 | } 97 | }); 98 | 99 | var component = fastn('foo'); 100 | 101 | component.extend('bar', {}); 102 | 103 | component.extend('baz', {}); 104 | 105 | t.ok(component.is('foo'), 'componant is foo'); 106 | t.ok(component.is('bar'), 'componant is bar'); 107 | t.ok(component.is('baz'), 'componant is baz'); 108 | }); 109 | 110 | test('cannot double-extend component', function(t){ 111 | 112 | t.plan(4); 113 | 114 | var fastn = createFastn({ 115 | foo: function(fastn, component, type, settings, children){ 116 | t.pass('Used foo constructor'); 117 | return component; 118 | }, 119 | bar: function(fastn, component, type, settings, children){ 120 | t.pass('Used bar constructor'); 121 | return component; 122 | } 123 | }); 124 | 125 | var component = fastn('foo'); 126 | 127 | component.extend('bar', {}); 128 | 129 | // Shouldn't cause another call to bar constructor. 130 | component.extend('bar', {}); 131 | 132 | t.ok(component.is('foo'), 'componant is foo'); 133 | t.ok(component.is('bar'), 'componant is bar'); 134 | }); -------------------------------------------------------------------------------- /test/components.js: -------------------------------------------------------------------------------- 1 | module.exports = function(components){ 2 | if(!components){ 3 | components = {}; 4 | } 5 | 6 | var genericComponent = require('../genericComponent'), 7 | textComponent = require('../textComponent'); 8 | 9 | // dont do fancy requestAnimationFrame scheduling that is hard to test. 10 | genericComponent.updateProperty = function(generic, property, update){ 11 | update(); 12 | }; 13 | 14 | genericComponent.createElement = function(tagName){ 15 | if(tagName instanceof Node){ 16 | return tagName; 17 | } 18 | return document.createElement(tagName); 19 | }; 20 | 21 | textComponent.createTextNode = document.createTextNode.bind(document); 22 | 23 | components._generic = genericComponent; 24 | components.list = require('../listComponent'); 25 | components.templater = require('../templaterComponent'); 26 | components.text = textComponent; 27 | 28 | return components; 29 | }; -------------------------------------------------------------------------------- /test/container.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | createFastn = require('./createFastn'); 3 | 4 | test('children are added', function(t){ 5 | 6 | t.plan(2); 7 | 8 | var fastn = createFastn(); 9 | 10 | var child, 11 | parent = fastn('div', 12 | child = fastn('span') 13 | ); 14 | 15 | parent.render(); 16 | 17 | document.body.appendChild(parent.element); 18 | 19 | t.equal(document.body.childNodes.length, 1); 20 | t.equal(parent.element.childNodes.length, 1); 21 | 22 | parent.element.remove(); 23 | parent.destroy(); 24 | 25 | }); 26 | 27 | test('undefined or null children are ignored', function(t){ 28 | 29 | t.plan(1); 30 | 31 | var fastn = createFastn(); 32 | 33 | var child, 34 | parent = fastn('div', 35 | child = fastn('span'), 36 | undefined, 37 | null 38 | ); 39 | 40 | parent.render(); 41 | 42 | document.body.appendChild(parent.element); 43 | 44 | t.equal(parent.element.childNodes.length, 1); 45 | 46 | parent.element.remove(); 47 | parent.destroy(); 48 | 49 | }); 50 | 51 | test('flatten children', function(t){ 52 | 53 | t.plan(1); 54 | 55 | var fastn = createFastn(); 56 | 57 | var parent = fastn('div', 58 | [fastn('span'), fastn('span')], 59 | fastn('span') 60 | ); 61 | 62 | parent.render(); 63 | 64 | document.body.appendChild(parent.element); 65 | 66 | t.equal(parent.element.childNodes.length, 3); 67 | 68 | parent.element.remove(); 69 | parent.destroy(); 70 | 71 | }); 72 | 73 | test('insert many after current', function(t){ 74 | 75 | t.plan(1); 76 | 77 | var fastn = createFastn(); 78 | 79 | var parent = fastn('div', 80 | fastn('span', '1'), 81 | fastn('span', '2') 82 | ); 83 | 84 | parent.insert( 85 | fastn('span', '3'), 86 | fastn('span', '4') 87 | ); 88 | 89 | parent.render(); 90 | 91 | document.body.appendChild(parent.element); 92 | 93 | t.equal(document.body.textContent, '1234'); 94 | 95 | parent.element.remove(); 96 | parent.destroy(); 97 | 98 | }); 99 | 100 | test('insert returns container', function(t){ 101 | 102 | t.plan(1); 103 | 104 | var fastn = createFastn(); 105 | 106 | var container = fastn('div'); 107 | 108 | t.equal(container.insert(fastn('span')), container); 109 | 110 | container.destroy(); 111 | 112 | }); 113 | 114 | test('children passed attachment', function(t){ 115 | 116 | t.plan(2); 117 | 118 | var fastn = createFastn(); 119 | 120 | var container = fastn('div', fastn.binding('foo')); 121 | 122 | container.render(); 123 | 124 | container.attach({foo: 'bar'}); 125 | 126 | t.equal(container.element.textContent, 'bar'); 127 | 128 | container.attach({foo: 'baz'}); 129 | 130 | t.equal(container.element.textContent, 'baz'); 131 | 132 | container.destroy(); 133 | 134 | }); 135 | 136 | test('children passed model change attachment', function(t){ 137 | 138 | t.plan(2); 139 | 140 | var fastn = createFastn(); 141 | 142 | var container = fastn('div', fastn.binding('foo')), 143 | model = new fastn.Model({foo: 'bar'}); 144 | 145 | container.render(); 146 | 147 | container.attach(model); 148 | 149 | t.equal(container.element.textContent, 'bar'); 150 | 151 | model.attach({foo: 'baz'}); 152 | 153 | t.equal(container.element.textContent, 'baz'); 154 | 155 | container.destroy(); 156 | 157 | }); 158 | 159 | test('insert undefined', function(t){ 160 | 161 | t.plan(1); 162 | 163 | var fastn = createFastn(); 164 | 165 | var container = fastn('div'); 166 | 167 | container.insert(undefined); 168 | 169 | t.equal(container.children().length, 0, 'Nothing was added'); 170 | 171 | }); 172 | 173 | test('insert undefined in array', function(t){ 174 | 175 | t.plan(1); 176 | 177 | var fastn = createFastn(); 178 | 179 | var container = fastn('div'); 180 | 181 | container.insert([1, undefined, 2]); 182 | 183 | t.equal(container.children().length, 2, 'Only values added'); 184 | 185 | }); 186 | 187 | test('insert mixed array', function(t){ 188 | 189 | t.plan(1); 190 | 191 | var fastn = createFastn(); 192 | 193 | var container = fastn('div'); 194 | 195 | container.insert([ 196 | undefined, 197 | null, 198 | false, 199 | 1, 200 | '2', 201 | NaN 202 | ]); 203 | 204 | t.equal(container.children().length, 3, 'Only values added'); 205 | 206 | }); 207 | 208 | test('insert destroyed component throws', function(t){ 209 | 210 | t.plan(1); 211 | 212 | var fastn = createFastn(); 213 | 214 | var container = fastn('div'); 215 | var child = fastn('div'); 216 | child.destroy(); 217 | 218 | t.throws(function(){ 219 | container.insert(child); 220 | }); 221 | }); -------------------------------------------------------------------------------- /test/createFastn.js: -------------------------------------------------------------------------------- 1 | var merge = require('flat-merge'); 2 | 3 | module.exports = function createFastn(components){ 4 | return require('../')(require('./components')(components)); 5 | }; -------------------------------------------------------------------------------- /test/customBinding.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | createBinding = require('../binding'), 3 | Enti = require('enti'); 4 | 5 | test('simple binding initialisation', function(t){ 6 | t.plan(3); 7 | 8 | var binding = createBinding('foo'); 9 | 10 | var model = {}, 11 | enti = new Enti(model); 12 | 13 | t.equal(binding(), undefined); 14 | 15 | enti.set('foo', 'bar'); 16 | 17 | t.equal(binding(), undefined); 18 | 19 | binding.attach(model); 20 | 21 | t.equal(binding(), 'bar'); 22 | }); 23 | 24 | test('simple binding set', function(t){ 25 | t.plan(2); 26 | 27 | var binding = createBinding('foo'); 28 | 29 | binding.attach({}); 30 | 31 | t.equal(binding(), undefined); 32 | 33 | binding('bazinga'); 34 | 35 | t.equal(binding(), 'bazinga'); 36 | }); 37 | 38 | test('simple binding event', function(t){ 39 | t.plan(3); 40 | 41 | var binding = createBinding('foo'); 42 | 43 | var model = {}, 44 | enti = new Enti(model); 45 | 46 | binding.attach(model); 47 | 48 | binding.once('change', function(value){ 49 | t.equal(value, 'bar'); 50 | t.equal(binding(), 'bar'); 51 | }); 52 | 53 | enti.set('foo', 'bar'); 54 | 55 | binding.once('detach', function(){ 56 | t.equal(binding(), undefined); 57 | }); 58 | 59 | binding.detach(); 60 | 61 | enti.set('foo', 'baz'); 62 | }); 63 | 64 | test('no model', function(t){ 65 | t.plan(3); 66 | 67 | var binding = createBinding('foo'); 68 | 69 | t.equal(binding(), undefined); 70 | 71 | binding.on('change', function(value){ 72 | t.equal(value, 'bar'); 73 | console.log(value) 74 | }); 75 | 76 | binding('bar'); 77 | console.log(binding()) 78 | 79 | t.equal(binding(), 'bar'); 80 | }); 81 | 82 | test('drill get', function(t){ 83 | t.plan(2); 84 | 85 | var data = { 86 | foo: { 87 | bar: 123 88 | } 89 | }, 90 | model = new Enti(data), 91 | binding = createBinding('foo.bar'); 92 | 93 | binding.attach(data); 94 | 95 | t.equal(binding(), 123); 96 | 97 | model.set('foo', { 98 | bar: 456 99 | }); 100 | 101 | t.equal(binding(), 456); 102 | }); 103 | 104 | test('drill change', function(t){ 105 | t.plan(1); 106 | 107 | var data = { 108 | foo: { 109 | bar: 123 110 | } 111 | }, 112 | model = new Enti(data), 113 | binding = createBinding('foo.bar'); 114 | 115 | binding.attach(data); 116 | 117 | binding.on('change', function(){ 118 | t.pass('target changed'); 119 | }); 120 | 121 | model.set('foo', { 122 | bar: 456 123 | }); 124 | }); 125 | 126 | test('drill attach', function(t){ 127 | t.plan(2); 128 | 129 | var data = { 130 | foo: { 131 | bar: 123 132 | } 133 | }, 134 | model = new Enti(data), 135 | binding = createBinding('foo.bar'); 136 | 137 | 138 | binding.once('change', function(value){ 139 | t.equal(value, 123); 140 | }); 141 | 142 | binding.attach(data); 143 | 144 | binding.once('change', function(value){ 145 | t.equal(value, 456); 146 | }); 147 | 148 | model.set('foo', { 149 | bar: 456 150 | }); 151 | }); 152 | 153 | test('drill set', function(t){ 154 | t.plan(1); 155 | 156 | var data = { 157 | foo: { 158 | bar: 123 159 | } 160 | }, 161 | model = new Enti(data), 162 | fooModel = new Enti(data.foo), 163 | binding = createBinding('foo.bar'); 164 | 165 | 166 | fooModel.on('bar', function(value){ 167 | t.equal(value, 456); 168 | }); 169 | 170 | binding.attach(data); 171 | 172 | binding(456); 173 | }); 174 | 175 | test('drill multiple', function(t){ 176 | t.plan(3); 177 | 178 | var data = { 179 | foo: { 180 | bar: 123 181 | } 182 | }, 183 | model = new Enti(data), 184 | fooModel = new Enti(data.foo), 185 | binding = createBinding('foo.bar'); 186 | 187 | 188 | fooModel.once('bar', function(value){ 189 | t.equal(value, 456); 190 | }); 191 | 192 | binding.attach(data); 193 | 194 | binding(456); 195 | 196 | binding.once('change', function(value){ 197 | t.equal(value, 789); 198 | }); 199 | 200 | fooModel.set('bar', 789); 201 | 202 | binding.once('change', function(value){ 203 | t.equal(value, 987); 204 | }); 205 | 206 | binding(987); 207 | }); 208 | 209 | test('fuse', function(t){ 210 | t.plan(2); 211 | 212 | var data = { 213 | foo: 1, 214 | bar: 2, 215 | baz: 3 216 | }, 217 | model = new Enti(data), 218 | binding = createBinding('foo', 'bar', 'baz', function(foo, bar, baz){ 219 | return foo + bar + baz; 220 | }); 221 | 222 | binding.attach(data); 223 | 224 | binding(2); 225 | 226 | binding.once('change', function(value){ 227 | t.equal(value, 7); 228 | }); 229 | 230 | model.set('bar', 3); 231 | 232 | binding.once('change', function(value){ 233 | t.equal(value, 3); 234 | }); 235 | 236 | binding(3); 237 | }); 238 | 239 | test('filter', function(t){ 240 | t.plan(2); 241 | 242 | var data = {}, 243 | model = new Enti(data), 244 | binding = createBinding('foo|*'); 245 | 246 | binding.attach(data); 247 | 248 | binding.on('change', function(value){ 249 | t.pass(); 250 | }); 251 | 252 | model.set('foo', []); 253 | 254 | Enti.set(data.foo, 0, {}); 255 | }); 256 | 257 | test('things', function(t){ 258 | t.plan(2); 259 | 260 | var data = {}, 261 | model = new Enti(data), 262 | binding = createBinding('foo|*.bar'); 263 | 264 | binding.attach(data); 265 | 266 | binding.on('change', function(value){ 267 | t.pass(); 268 | }); 269 | 270 | model.set('foo', [{}]); 271 | 272 | Enti.set(data.foo[0], 'bar', true); 273 | }); 274 | 275 | test('clone', function(t){ 276 | t.plan(4); 277 | 278 | var data1 = {foo:1}, 279 | data2 = {foo:2}, 280 | binding = createBinding('foo'); 281 | 282 | binding.attach(data1); 283 | 284 | t.equal(binding(), 1, 'Original binding has correct data'); 285 | 286 | var newBinding = binding.clone(); 287 | 288 | t.equal(newBinding(), undefined, 'New binding has no data'); 289 | 290 | newBinding.attach(data2); 291 | 292 | t.equal(newBinding(), 2, 'New binding has new data'); 293 | 294 | t.equal(binding(), 1, 'Original binding still has original data'); 295 | }); 296 | 297 | test('clone with attachment', function(t){ 298 | t.plan(2); 299 | 300 | var data1 = {foo:1}, 301 | binding = createBinding('foo'); 302 | 303 | binding.attach(data1); 304 | 305 | t.equal(binding(), 1, 'Original binding has correct data'); 306 | 307 | var newBinding = binding.clone(true); 308 | 309 | t.equal(newBinding(), 1, 'New binding has same data'); 310 | }); 311 | 312 | test('clone fuse', function(t){ 313 | t.plan(2); 314 | 315 | var data1 = {foo:1, bar:2}, 316 | binding = createBinding('foo', 'bar', function(foo, bar){ 317 | return foo + bar; 318 | }); 319 | 320 | binding.attach(data1); 321 | 322 | t.equal(binding(), 3, 'Original binding has correct data'); 323 | 324 | var newBinding = binding.clone(true); 325 | 326 | t.equal(newBinding(), 3, 'New binding has same data'); 327 | }); 328 | 329 | test('binding as a bindings target', function(t){ 330 | t.plan(1); 331 | 332 | var binding1 = createBinding('foo'), 333 | binding2 = createBinding('bar'); 334 | 335 | binding1(binding2); 336 | 337 | t.equal(binding1(), binding2, 'binding1 value correctly set to binding2'); 338 | }); 339 | 340 | test('binding as own target', function(t){ 341 | t.plan(1); 342 | 343 | var binding = createBinding('foo'); 344 | 345 | binding(binding); 346 | 347 | t.equal(binding(), binding, 'binding value correctly set to self'); 348 | }); 349 | 350 | test('value-only binding', function(t){ 351 | t.plan(1); 352 | 353 | var binding = createBinding(); 354 | 355 | binding('foo'); 356 | 357 | t.equal(binding(), 'foo', 'binding value correctly set to foo'); 358 | }); 359 | 360 | test('value-only binding cannot be attached', function(t){ 361 | t.plan(1); 362 | 363 | var binding = createBinding(); 364 | 365 | binding('foo'); 366 | 367 | binding.attach({ 368 | value: 'bar' 369 | }); 370 | 371 | t.equal(binding(), 'foo', 'binding value correctly set to foo'); 372 | }); 373 | 374 | test('destroy', function(t){ 375 | t.plan(1); 376 | 377 | var binding = createBinding().on('change', function(){ 378 | t.pass('binding changed'); 379 | }); 380 | 381 | binding('foo'); 382 | 383 | binding.destroy(); 384 | 385 | binding('bar'); 386 | }); 387 | 388 | test('soft destroy', function(t){ 389 | t.plan(2); 390 | 391 | var binding = createBinding().on('change', function(){ 392 | t.pass('binding changed'); 393 | }); 394 | 395 | binding('foo'); 396 | 397 | binding.destroy(true); 398 | 399 | binding('bar'); 400 | }); 401 | 402 | test('soft destroy 2', function(t){ 403 | t.plan(1); 404 | 405 | function changeHandler(){ 406 | t.pass('binding changed'); 407 | } 408 | 409 | var binding = createBinding().on('change', changeHandler); 410 | 411 | binding('foo'); 412 | 413 | binding.removeListener('change', changeHandler); 414 | binding.destroy(true); 415 | 416 | binding('bar'); 417 | }); 418 | 419 | test('model attach', function(t){ 420 | t.plan(2); 421 | 422 | var model = new Enti(); 423 | 424 | var binding = createBinding('a'); 425 | 426 | binding.attach(model); 427 | 428 | t.equal(binding(), undefined); 429 | 430 | model.attach({ 431 | a: 2 432 | }); 433 | 434 | t.equal(binding(), 2); 435 | 436 | }); -------------------------------------------------------------------------------- /test/customModel.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | EventEmitter = require('events'), 3 | createFastn = require('../index'); 4 | 5 | var allModels = new Set(); 6 | 7 | function CustomModel(instance){ 8 | allModels.add(this); 9 | 10 | this._model = instance; 11 | 12 | this; 13 | 14 | return this; 15 | } 16 | CustomModel.get = function(target, key){ 17 | var match = key.match(matchKeys); 18 | 19 | if(!match){ 20 | return; 21 | } 22 | 23 | while(match[2]){ 24 | if(!target){ 25 | return; 26 | } 27 | 28 | target = target[match[1]]; 29 | match = match[2].match(matchKeys); 30 | } 31 | 32 | if(!target){ 33 | return; 34 | } 35 | 36 | return target[match[1]]; 37 | }; 38 | CustomModel.set = function(target, key, value){ 39 | var instance = target, 40 | match = key.match(matchKeys); 41 | 42 | if(!match){ 43 | return; 44 | } 45 | 46 | while(match[2]){ 47 | if(!target){ 48 | return; 49 | } 50 | 51 | target = target[match[1]]; 52 | match = match[2].match(matchKeys); 53 | } 54 | 55 | if(!target){ 56 | return; 57 | } 58 | 59 | target[match[1]] = value; 60 | allModels.forEach(function(model){ 61 | if(model.isAttached() && model._model === instance){ 62 | model._events && Object.keys(model._events).forEach(function(key){ 63 | if(model.get(key.match(/(.*?)\./)[1]) === target){ 64 | model.emit(key, value); 65 | } 66 | }); 67 | } 68 | }); 69 | }; 70 | CustomModel.remove = function(target, key){ 71 | var instance = target, 72 | match = key.match(matchKeys); 73 | 74 | if(!match){ 75 | return; 76 | } 77 | 78 | while(match[2]){ 79 | if(!target){ 80 | return; 81 | } 82 | 83 | target = target[match[1]]; 84 | match = match[2].match(matchKeys); 85 | } 86 | 87 | if(!target){ 88 | return; 89 | } 90 | 91 | delete target[match[1]]; 92 | allModels.forEach(function(model){ 93 | if(model.isAttached() && model._model === instance){ 94 | model._events && Object.keys(model._events).forEach(function(key){ 95 | if(model.get(key.match(/(.*?)\./)[1]) === target){ 96 | model.emit(key); 97 | } 98 | }); 99 | } 100 | }); 101 | }; 102 | CustomModel.prototype = Object.create(EventEmitter.prototype); 103 | CustomModel.prototype.constructor = CustomModel; 104 | CustomModel.prototype._maxListeners = 100; 105 | CustomModel.prototype.constructor = CustomModel; 106 | CustomModel.prototype.attach = function(instance){ 107 | if(this._model !== instance){ 108 | this.detach(); 109 | } 110 | 111 | allModels.add(this); 112 | this._attached = true; 113 | this._model = instance; 114 | this.emit('attach', instance); 115 | }; 116 | CustomModel.prototype.detach = function(){ 117 | allModels.delete(this); 118 | 119 | this._model = {}; 120 | this._attached = false; 121 | this.emit('detach'); 122 | }; 123 | CustomModel.prototype.destroy = function(){ 124 | this.detach(); 125 | this._events = null; 126 | this.emit('destroy'); 127 | }; 128 | var matchKeys = /(.*?)(?:\.(.*)|$)/; 129 | CustomModel.prototype.get = function(key){ 130 | return CustomModel.get(this._model, key); 131 | }; 132 | CustomModel.prototype.set = function(key, value){ 133 | return CustomModel.set(this._model, key, value); 134 | }; 135 | CustomModel.prototype.remove = function(key){ 136 | return CustomModel.remove(this._model, key); 137 | }; 138 | CustomModel.prototype.isAttached = function(){ 139 | return !!this._model; 140 | }; 141 | CustomModel.isModel = function(target){ 142 | return target && target instanceof CustomModel; 143 | }; 144 | 145 | 146 | test('binding with custom model', function(t){ 147 | t.plan(4); 148 | 149 | var fastn = createFastn({}); 150 | fastn.Model = CustomModel; 151 | fastn.isModel = CustomModel.isModel; 152 | 153 | var binding = fastn.binding('foo'); 154 | 155 | var model = {}, 156 | enti = new CustomModel(model); 157 | 158 | t.equal(binding(), undefined); 159 | 160 | enti.set('foo', 'bar'); 161 | 162 | t.equal(binding(), undefined); 163 | 164 | binding.attach(model); 165 | 166 | t.equal(binding(), 'bar'); 167 | 168 | binding.detach(); 169 | 170 | t.equal(binding(), undefined); 171 | }); -------------------------------------------------------------------------------- /test/document.js: -------------------------------------------------------------------------------- 1 | module.exports = function(){ 2 | var domLite = require('dom-lightning'); 3 | 4 | document = domLite.document; 5 | document.body = document.createElement('body'); 6 | 7 | global.Node = domLite.Node; 8 | global.document = document; 9 | global.Element = domLite.Element; 10 | global.HTMLElement = domLite.HTMLElement; 11 | }; -------------------------------------------------------------------------------- /test/fancyProps.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | crel = require('crel'), 3 | fancyProps = require('../fancyProps'); 4 | 5 | test('date input', function(t){ 6 | 7 | t.plan(2); 8 | 9 | var input = crel('input', {type: 'date'}); 10 | 11 | t.equal(fancyProps.value({}, input), null); 12 | 13 | fancyProps.value({}, input, new Date('2000-1-1')); 14 | 15 | t.equal(fancyProps.value({}, input).toString(), new Date('2000-1-1').toString()); 16 | }); 17 | 18 | test('class', function(t){ 19 | 20 | t.plan(3); 21 | 22 | var component = {}, 23 | span = crel('span'); 24 | 25 | t.equal(fancyProps.class(component, span), ''); 26 | 27 | fancyProps.class(component, span, 'foo'); 28 | 29 | t.equal(fancyProps.class(component, span), 'foo'); 30 | 31 | fancyProps.class(component, span, ['bar']); 32 | 33 | t.equal(fancyProps.class(component, span), 'bar'); 34 | }); 35 | 36 | test('class 2', function(t){ 37 | 38 | t.plan(6); 39 | 40 | var component = {}, 41 | span = crel('span', {class: 'majigger'}); 42 | 43 | t.equal(fancyProps.class(component, span), ''); 44 | t.equal(span.className, 'majigger'); 45 | 46 | fancyProps.class(component, span, 'foo'); 47 | 48 | t.equal(fancyProps.class(component, span), 'foo'); 49 | t.equal(span.className, 'majigger foo'); 50 | 51 | span.className += ' whatsits'; 52 | 53 | fancyProps.class(component, span, ['bar']); 54 | 55 | t.equal(fancyProps.class(component, span), 'bar'); 56 | t.equal(span.className, 'majigger whatsits bar'); 57 | }); 58 | 59 | test('style string', function(t){ 60 | 61 | t.plan(4); 62 | 63 | var component = {}, 64 | span = crel('span'); 65 | 66 | t.equal(fancyProps.style(component, span).background, ''); 67 | t.equal(span.style.background, ''); 68 | 69 | fancyProps.style(component, span, 'background: red'); 70 | 71 | t.equal(fancyProps.style(component, span).background, 'red'); 72 | t.equal(span.style.background, 'red'); 73 | }); 74 | 75 | test('style object', function(t){ 76 | 77 | t.plan(4); 78 | 79 | var component = {}, 80 | span = crel('span'); 81 | 82 | t.equal(fancyProps.style(component, span).background, ''); 83 | t.equal(span.style.background, ''); 84 | 85 | fancyProps.style(component, span, { background: 'red' }); 86 | 87 | t.equal(fancyProps.style(component, span).background, 'red'); 88 | t.equal(span.style.background, 'red'); 89 | }); -------------------------------------------------------------------------------- /test/firmer.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | firmer = require('../firmer'); 3 | 4 | test('default (0) firmness', function(t){ 5 | 6 | t.plan(2); 7 | 8 | var entitiy = {_firm:0}; 9 | 10 | t.notOk(firmer(entitiy, 1)); 11 | t.notOk(firmer(entitiy, 0)); 12 | }); 13 | 14 | test('template (1) firmness', function(t){ 15 | 16 | t.plan(2); 17 | 18 | var entitiy = {_firm:1}; 19 | 20 | t.notOk(firmer(entitiy, 1)); 21 | t.ok(firmer(entitiy, 0)); 22 | }); 23 | 24 | test('custom (2) firmness', function(t){ 25 | 26 | t.plan(2); 27 | 28 | var entitiy = {_firm:2}; 29 | 30 | t.ok(firmer(entitiy, 1)); 31 | t.ok(firmer(entitiy, 0)); 32 | }); 33 | 34 | test('attach() (undefined) firmness', function(t){ 35 | 36 | t.plan(3); 37 | 38 | var entitiy = {_firm:undefined}; 39 | 40 | t.ok(firmer(entitiy, 0)); 41 | t.ok(firmer(entitiy, 1)); 42 | t.ok(firmer(entitiy, Infinity)); 43 | }); -------------------------------------------------------------------------------- /test/generic.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | crel = require('crel'), 3 | createFastn = require('./createFastn'); 4 | 5 | test('div', function(t){ 6 | 7 | t.plan(2); 8 | 9 | var fastn = createFastn(); 10 | 11 | var div = fastn('div'); 12 | 13 | div.render(); 14 | 15 | document.body.appendChild(div.element); 16 | 17 | t.equal(document.body.childNodes.length, 1); 18 | t.equal(document.body.childNodes[0].tagName, 'DIV'); 19 | 20 | div.element.remove(); 21 | div.destroy(); 22 | 23 | }); 24 | 25 | test('undefined attribute is removed', function(t){ 26 | 27 | t.plan(2); 28 | 29 | var fastn = createFastn(); 30 | 31 | var div = fastn('div', { someattr: true }); 32 | 33 | div.render(); 34 | 35 | t.equal(div.element.hasAttribute('someattr'), true); 36 | 37 | div.someattr(undefined); 38 | 39 | t.equal(div.element.hasAttribute('someattr'), false, 'undefined attribute is removed'); 40 | 41 | div.destroy(); 42 | }); 43 | 44 | 45 | test('special properties - input value - undefined', function(t){ 46 | 47 | t.plan(3); 48 | 49 | var fastn = createFastn(); 50 | 51 | var input = fastn('input', {value: undefined}); 52 | 53 | input.render(); 54 | 55 | document.body.appendChild(input.element); 56 | 57 | t.equal(document.body.childNodes.length, 1); 58 | t.equal(document.body.childNodes[0].tagName, 'INPUT'); 59 | t.equal(document.body.childNodes[0].value, ''); 60 | 61 | input.element.remove(); 62 | input.destroy(); 63 | 64 | }); 65 | 66 | test('special properties - input value - dates', function(t){ 67 | 68 | t.plan(8); 69 | 70 | var fastn = createFastn(); 71 | 72 | var input = fastn('input', { 73 | type: 'date', 74 | value: new Date('2015/01/01'), 75 | onchange: 'value:value', 76 | onclick: 'value:value' // so I can trigger events.. 77 | }); 78 | 79 | input.render(); 80 | 81 | document.body.appendChild(input.element); 82 | 83 | t.equal(document.body.childNodes.length, 1, 'node added'); 84 | t.equal(document.body.childNodes[0].tagName, 'INPUT', 'correct tagName'); 85 | t.equal(document.body.childNodes[0].value, '2015-01-01', 'correct initial input.value'); 86 | t.deepEqual(input.value(), new Date('2015/01/01'), 'correct initial property()'); 87 | 88 | input.value(new Date('2015/02/02')); 89 | 90 | t.equal(document.body.childNodes[0].value, '2015-02-02', 'correctly set new input.value'); 91 | t.deepEqual(input.value(), new Date('2015/02/02'), 'correctly set new property()'); 92 | 93 | input.element.value = '2016-02-02'; 94 | input.element.click(); 95 | 96 | t.equal(document.body.childNodes[0].value, '2016-02-02', 'correctly set new input.value 2'); 97 | t.deepEqual(input.value(), new Date('2016/02/02'), 'correctly set new property() 2'); 98 | 99 | input.element.remove(); 100 | input.destroy(); 101 | 102 | }); 103 | 104 | test('special properties - disabled', function(t){ 105 | 106 | t.plan(4); 107 | 108 | var fastn = createFastn(); 109 | 110 | var button = fastn('button', { 111 | type: 'button', 112 | disabled: false 113 | }); 114 | 115 | button.render(); 116 | 117 | document.body.appendChild(button.element); 118 | 119 | t.equal(document.body.childNodes.length, 1); 120 | t.equal(document.body.childNodes[0].tagName, 'BUTTON'); 121 | t.equal(document.body.childNodes[0].getAttribute('disabled'), null); 122 | 123 | button.disabled(true); 124 | 125 | t.equal(document.body.childNodes[0].getAttribute('disabled'), 'disabled'); 126 | 127 | button.element.remove(); 128 | button.destroy(); 129 | 130 | }); 131 | 132 | test('special properties - textContent', function(t){ 133 | 134 | t.plan(4); 135 | 136 | var fastn = createFastn(); 137 | 138 | var label = fastn('label', { 139 | textContent: 'foo' 140 | }); 141 | 142 | label.render(); 143 | 144 | document.body.appendChild(label.element); 145 | 146 | t.equal(document.body.childNodes.length, 1); 147 | t.equal(document.body.childNodes[0].tagName, 'LABEL'); 148 | t.equal(document.body.childNodes[0].textContent, 'foo'); 149 | 150 | label.textContent(null); 151 | 152 | t.equal(document.body.childNodes[0].textContent, ''); 153 | 154 | label.element.remove(); 155 | label.destroy(); 156 | 157 | }); 158 | 159 | test('special properties - innerHTML', function(t){ 160 | 161 | t.plan(4); 162 | 163 | var fastn = createFastn(); 164 | 165 | var label = fastn('label', { 166 | innerHTML: 'foo' 167 | }); 168 | 169 | label.render(); 170 | 171 | document.body.appendChild(label.element); 172 | 173 | t.equal(document.body.childNodes.length, 1); 174 | t.equal(document.body.childNodes[0].tagName, 'LABEL'); 175 | t.equal(document.body.childNodes[0].innerHTML, 'foo'); 176 | 177 | label.innerHTML(null); 178 | 179 | t.equal(document.body.childNodes[0].innerHTML, ''); 180 | 181 | label.element.remove(); 182 | label.destroy(); 183 | 184 | }); 185 | 186 | test('preexisting element', function(t){ 187 | 188 | t.plan(4); 189 | 190 | var fastn = createFastn(); 191 | 192 | var element = crel('label'), 193 | label = fastn(element, { 194 | textContent: 'foo' 195 | }); 196 | 197 | label.render(); 198 | 199 | document.body.appendChild(label.element); 200 | 201 | t.equal(document.body.childNodes.length, 1); 202 | t.equal(document.body.childNodes[0].tagName, 'LABEL'); 203 | t.equal(document.body.childNodes[0].textContent, 'foo'); 204 | 205 | label.textContent(null); 206 | 207 | t.equal(document.body.childNodes[0].textContent, ''); 208 | 209 | label.element.remove(); 210 | label.destroy(); 211 | 212 | }); 213 | 214 | test('DOM children', function(t){ 215 | 216 | t.plan(3); 217 | 218 | var fastn = createFastn(); 219 | 220 | var label = fastn('div', 221 | crel('h1', 'DOM Child') 222 | ); 223 | 224 | label.render(); 225 | 226 | document.body.appendChild(label.element); 227 | 228 | t.equal(document.body.childNodes.length, 1); 229 | t.equal(document.body.childNodes[0].tagName, 'DIV'); 230 | t.equal(document.body.childNodes[0].textContent, 'DOM Child'); 231 | 232 | label.element.remove(); 233 | label.destroy(); 234 | 235 | }); 236 | 237 | test('same scope', function(t){ 238 | 239 | t.plan(4); 240 | 241 | var fastn = createFastn(); 242 | 243 | var thing = fastn('label', {}, fastn.binding('x')); 244 | 245 | thing.render(); 246 | document.body.appendChild(thing.element); 247 | 248 | t.equal(document.body.childNodes.length, 1); 249 | t.equal(document.body.childNodes[0].tagName, 'LABEL'); 250 | 251 | thing.attach({ 252 | x: 10 253 | }); 254 | 255 | t.equal(document.body.childNodes[0].textContent, '10'); 256 | 257 | thing.attach({ 258 | x: 20 259 | }); 260 | 261 | t.equal(document.body.childNodes[0].textContent, '20'); 262 | 263 | thing.element.remove(); 264 | thing.destroy(); 265 | 266 | }); 267 | 268 | test('default type', function(t){ 269 | 270 | t.plan(1); 271 | 272 | var fastn = createFastn(); 273 | 274 | var thing = fastn('_generic').render(); 275 | 276 | t.equal(thing.element.tagName, 'DIV'); 277 | 278 | thing.destroy(); 279 | 280 | }); 281 | 282 | test('override type', function(t){ 283 | 284 | t.plan(1); 285 | 286 | var fastn = createFastn(); 287 | 288 | var thing = fastn('span:div:section').render(); 289 | 290 | t.equal(thing.element.tagName, 'SECTION'); 291 | 292 | thing.destroy(); 293 | 294 | }); 295 | 296 | test('custom fancyProps', function(t){ 297 | 298 | t.plan(3); 299 | 300 | var fastn = createFastn({ 301 | custom: function(fastn, component, type, settings, children){ 302 | // Map all settings to data-{name} as an example 303 | component.extend('_generic', settings, children); 304 | component._fancyProps = function(attribute){ 305 | if(attribute === 'ignore'){ 306 | return; 307 | } 308 | 309 | return function(component, element, value){ 310 | if(arguments.length < 3){ 311 | return element.getAttribute('data-' + attribute); 312 | } 313 | 314 | return element.setAttribute('data-' + attribute, value); 315 | } 316 | } 317 | return component; 318 | } 319 | }); 320 | 321 | var thing = fastn('div:custom', { property: 'foo', ignore: 'bar' }).render(); 322 | 323 | t.equal(thing.element.tagName, 'DIV'); 324 | t.equal(thing.element.getAttribute('data-property'), 'foo'); 325 | t.equal(thing.element.getAttribute('ignore'), 'bar'); 326 | 327 | thing.destroy(); 328 | 329 | }); 330 | 331 | test('event handling - auto handler', function(t){ 332 | 333 | t.plan(1); 334 | 335 | var fastn = createFastn(); 336 | 337 | var input = fastn('input', { 338 | value: 'a', 339 | onclick: 'value:value', 340 | }); 341 | 342 | input.render(); 343 | 344 | document.body.appendChild(input.element); 345 | 346 | input.element.value = 'b'; 347 | input.element.click(); 348 | 349 | t.equal(input.value(), 'b') 350 | 351 | input.element.remove(); 352 | input.destroy(); 353 | 354 | }); 355 | 356 | test('event handling - function handler', function(t){ 357 | 358 | t.plan(1); 359 | 360 | var fastn = createFastn(); 361 | 362 | var button = fastn('button', { 363 | onclick: (event, scope) => t.pass('recieved click') 364 | }); 365 | 366 | button.render(); 367 | 368 | document.body.appendChild(button.element); 369 | 370 | button.element.click(); 371 | 372 | button.element.remove(); 373 | button.destroy(); 374 | 375 | }); 376 | 377 | test('event handling - function handler - this', function(t){ 378 | 379 | t.plan(1); 380 | 381 | var fastn = createFastn(); 382 | 383 | var input = fastn('input', { 384 | value: 'a', 385 | onclick: function(event, scope){ this.value('b') } 386 | }); 387 | 388 | input.render(); 389 | 390 | document.body.appendChild(input.element); 391 | 392 | input.element.value = 'b'; 393 | input.element.click(); 394 | 395 | t.equal(input.value(), 'b') 396 | 397 | input.element.remove(); 398 | input.destroy(); 399 | 400 | }); 401 | 402 | test('event handling - component handler', function(t){ 403 | 404 | t.plan(1); 405 | 406 | var fastn = createFastn(); 407 | 408 | var button = fastn('button') 409 | .on('click', (event, scope) => t.pass('recieved click')) 410 | 411 | button.render(); 412 | 413 | document.body.appendChild(button.element); 414 | 415 | button.element.click(); 416 | 417 | button.element.remove(); 418 | button.destroy(); 419 | 420 | }); 421 | 422 | test('event handling - function handler - this', function(t){ 423 | 424 | t.plan(1); 425 | 426 | var fastn = createFastn(); 427 | 428 | var input = fastn('input', { 429 | value: 'a' 430 | }) 431 | .on('click', function(event, scope){ this.value('b') }) 432 | 433 | input.render(); 434 | 435 | document.body.appendChild(input.element); 436 | 437 | input.element.value = 'b'; 438 | input.element.click(); 439 | 440 | t.equal(input.value(), 'b') 441 | 442 | input.element.remove(); 443 | input.destroy(); 444 | 445 | }); 446 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | 3 | function run(){ 4 | document.body.innerHTML = ''; 5 | 6 | require('./firmer.js'); 7 | require('./binding.js'); 8 | require('./property.js'); 9 | require('./component.js'); 10 | require('./text.js'); 11 | require('./list.js'); 12 | require('./templater.js'); 13 | require('./container.js'); 14 | require('./generic.js'); 15 | require('./attach.js'); 16 | require('./fancyProps.js'); 17 | require('./customModel.js'); 18 | } 19 | 20 | if(typeof document !== 'undefined'){ 21 | window.onload = run; 22 | }else{ 23 | require('./document')(); 24 | run(); 25 | } -------------------------------------------------------------------------------- /test/list.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | consoleWatch = require('console-watch'), 3 | Enti = require('enti'), 4 | createFastn = require('./createFastn'); 5 | 6 | test('value items', function(t){ 7 | 8 | t.plan(1); 9 | 10 | var fastn = createFastn(); 11 | 12 | var list = fastn('list', { 13 | items: [1,2,3,4], 14 | template: function(model){ 15 | return fastn.binding('item'); 16 | } 17 | }); 18 | 19 | list.render(); 20 | 21 | document.body.appendChild(list.element); 22 | 23 | t.equal(document.body.textContent, '1234'); 24 | 25 | list.element.remove(); 26 | list.destroy(); 27 | 28 | }); 29 | 30 | test('value items duplicate values', function(t){ 31 | 32 | t.plan(1); 33 | 34 | var fastn = createFastn(); 35 | 36 | var list = fastn('list', { 37 | items: [1,1,2,2], 38 | template: function(model){ 39 | return fastn.binding('item'); 40 | } 41 | }); 42 | 43 | list.render(); 44 | 45 | document.body.appendChild(list.element); 46 | 47 | t.equal(document.body.textContent, '1122'); 48 | 49 | list.element.remove(); 50 | list.destroy(); 51 | 52 | }); 53 | 54 | test('bound items', function(t){ 55 | 56 | t.plan(1); 57 | 58 | var fastn = createFastn(); 59 | 60 | var list = fastn('list', { 61 | items: fastn.binding('items|*'), 62 | template: function(model){ 63 | return fastn.binding('item'); 64 | } 65 | }); 66 | 67 | list.attach({ 68 | items: [1,2,3,4] 69 | }); 70 | list.render(); 71 | 72 | document.body.appendChild(list.element); 73 | 74 | t.equal(document.body.textContent, '1234'); 75 | 76 | list.element.remove(); 77 | list.destroy(); 78 | 79 | }); 80 | 81 | 82 | test('bound items changing', function(t){ 83 | 84 | t.plan(2); 85 | 86 | var fastn = createFastn(); 87 | 88 | var list = fastn('list', { 89 | items: fastn.binding('items|*'), 90 | template: function(model){ 91 | return fastn.binding('item'); 92 | } 93 | }), 94 | model = new Enti({ 95 | items: [1,2,3,4] 96 | }); 97 | 98 | list.attach(model); 99 | list.render(); 100 | 101 | document.body.appendChild(list.element); 102 | 103 | t.equal(document.body.textContent, '1234'); 104 | 105 | model.set('items.1', 5); 106 | 107 | t.equal(document.body.textContent, '1534'); 108 | 109 | list.element.remove(); 110 | list.destroy(); 111 | 112 | }); 113 | 114 | test('bound items add', function(t){ 115 | 116 | t.plan(2); 117 | 118 | var fastn = createFastn(); 119 | 120 | var list = fastn('list', { 121 | items: fastn.binding('items|*'), 122 | template: function(model){ 123 | return fastn.binding('item'); 124 | } 125 | }), 126 | model = new Enti({ 127 | items: [1,2,3,4] 128 | }); 129 | 130 | list.attach(model); 131 | list.render(); 132 | 133 | document.body.appendChild(list.element); 134 | 135 | t.equal(document.body.textContent, '1234'); 136 | 137 | model.set('items.4', 5); 138 | 139 | t.equal(document.body.textContent, '12345'); 140 | 141 | list.element.remove(); 142 | list.destroy(); 143 | 144 | }); 145 | 146 | test('bound items remove', function(t){ 147 | 148 | t.plan(2); 149 | 150 | var fastn = createFastn(); 151 | 152 | var list = fastn('list', { 153 | items: fastn.binding('items|*'), 154 | template: function(model){ 155 | return fastn.binding('item'); 156 | } 157 | }), 158 | model = new Enti({ 159 | items: [1,2,3,4] 160 | }); 161 | 162 | list.attach(model); 163 | list.render(); 164 | 165 | document.body.appendChild(list.element); 166 | 167 | t.equal(document.body.textContent, '1234'); 168 | 169 | model.remove('items.3'); 170 | 171 | t.equal(document.body.textContent, '123'); 172 | 173 | list.element.remove(); 174 | list.destroy(); 175 | 176 | }); 177 | 178 | test('bound items remove same instance', function(t){ 179 | 180 | t.plan(3); 181 | 182 | var fastn = createFastn(); 183 | 184 | var list = fastn('list', { 185 | items: fastn.binding('items|*'), 186 | template: function(model){ 187 | return fastn.binding('item.name'); 188 | } 189 | }), 190 | model = new Enti({ 191 | items: [] 192 | }); 193 | 194 | var testItem = { 195 | name: 'foo' 196 | }; 197 | 198 | model.push('items', testItem); 199 | model.push('items', testItem); 200 | 201 | list.attach(model); 202 | list.render(); 203 | 204 | document.body.appendChild(list.element); 205 | 206 | t.equal(document.body.textContent, 'foofoo'); 207 | 208 | model.remove('items.0'); 209 | 210 | t.equal(document.body.textContent, 'foo'); 211 | 212 | model.remove('items.0'); 213 | 214 | t.equal(document.body.textContent, ''); 215 | 216 | list.element.remove(); 217 | list.destroy(); 218 | 219 | }); 220 | 221 | test('null items', function(t){ 222 | 223 | t.plan(1); 224 | 225 | var fastn = createFastn(); 226 | 227 | var list = fastn('list', { 228 | items: null, 229 | template: function(model){ 230 | return fastn.binding('item'); 231 | } 232 | }); 233 | 234 | list.render(); 235 | 236 | document.body.appendChild(list.element); 237 | 238 | t.equal(document.body.textContent, ''); 239 | 240 | list.element.remove(); 241 | list.destroy(); 242 | 243 | }); 244 | 245 | test('null template', function(t){ 246 | 247 | t.plan(1); 248 | 249 | var fastn = createFastn(); 250 | 251 | var list = fastn('list', { 252 | items: [1,2,3,4], 253 | template: function(model){} 254 | }); 255 | 256 | list.render(); 257 | 258 | document.body.appendChild(list.element); 259 | 260 | t.equal(document.body.textContent, ''); 261 | 262 | list.element.remove(); 263 | list.destroy(); 264 | 265 | }); 266 | 267 | test('array to undefined', function(t){ 268 | 269 | t.plan(2); 270 | 271 | var fastn = createFastn(); 272 | 273 | var list = fastn('list', { 274 | items: fastn.binding('items|*'), 275 | template: function(model){ 276 | return fastn.binding('item'); 277 | } 278 | }), 279 | model = new Enti({ 280 | items: [1,2,3,4] 281 | }); 282 | 283 | list.attach(model); 284 | list.render(); 285 | 286 | document.body.appendChild(list.element); 287 | 288 | t.equal(document.body.textContent, '1234'); 289 | 290 | model.remove('items'); 291 | 292 | t.equal(document.body.textContent, ''); 293 | 294 | list.element.remove(); 295 | list.destroy(); 296 | 297 | }); 298 | 299 | test('array to null', function(t){ 300 | 301 | t.plan(2); 302 | 303 | var fastn = createFastn(); 304 | 305 | var list = fastn('list', { 306 | items: fastn.binding('items|*'), 307 | template: function(model){ 308 | return fastn.binding('item'); 309 | } 310 | }), 311 | model = new Enti({ 312 | items: [1,2,3,4] 313 | }); 314 | 315 | list.attach(model); 316 | list.render(); 317 | 318 | document.body.appendChild(list.element); 319 | 320 | t.equal(document.body.textContent, '1234'); 321 | 322 | model.set('items', null); 323 | 324 | t.equal(document.body.textContent, ''); 325 | 326 | list.element.remove(); 327 | list.destroy(); 328 | 329 | }); 330 | 331 | test('reattach list with templates', function(t){ 332 | 333 | t.plan(3); 334 | 335 | var fastn = createFastn(); 336 | 337 | var data = {foo: [ 338 | {a:1} 339 | ]}, 340 | list = fastn('list', { 341 | items: fastn.binding('.|*'), 342 | template: function(model, scope, lastTemplate){ 343 | return fastn.binding('item.a'); 344 | } 345 | }) 346 | .attach(data) 347 | .binding('foo'); 348 | 349 | list.render(); 350 | 351 | document.body.appendChild(list.element); 352 | 353 | t.equal(document.body.textContent, '1'); 354 | 355 | fastn.Model.set(data, 'foo', [{ 356 | a: 2 357 | }]); 358 | 359 | t.equal(document.body.textContent, '2'); 360 | 361 | fastn.Model.set(data, 'foo', [{ 362 | a: 3 363 | }]); 364 | 365 | t.equal(document.body.textContent, '3'); 366 | 367 | list.element.remove(); 368 | list.destroy(); 369 | 370 | }); 371 | 372 | test('dynamic template removed', function(t){ 373 | 374 | t.plan(2); 375 | 376 | var fastn = createFastn(); 377 | 378 | var templateBinding = fastn.binding(); 379 | templateBinding(function(model){ 380 | return fastn.binding('item'); 381 | }); 382 | 383 | var list = fastn('list', { 384 | items: [1,2,3,4], 385 | template: templateBinding 386 | }); 387 | 388 | list.render(); 389 | 390 | document.body.appendChild(list.element); 391 | 392 | t.equal(document.body.textContent, '1234'); 393 | 394 | templateBinding(null); 395 | 396 | t.equal(document.body.textContent, ''); 397 | 398 | list.element.remove(); 399 | list.destroy(); 400 | 401 | }); 402 | 403 | test('dynamic template', function(t){ 404 | 405 | t.plan(2); 406 | 407 | var fastn = createFastn(); 408 | 409 | var templateBinding = fastn.binding(); 410 | templateBinding(function(model){ 411 | return fastn.binding('item'); 412 | }); 413 | 414 | var list = fastn('list', { 415 | items: [1,2,3,4], 416 | template: templateBinding 417 | }); 418 | 419 | list.render(); 420 | 421 | document.body.appendChild(list.element); 422 | 423 | t.equal(document.body.textContent, '1234'); 424 | 425 | templateBinding(function(model){ 426 | return '*'; 427 | }); 428 | 429 | t.equal(document.body.textContent, '****'); 430 | 431 | list.element.remove(); 432 | list.destroy(); 433 | 434 | }); 435 | 436 | test('object item keys', function(t){ 437 | 438 | t.plan(2); 439 | 440 | var fastn = createFastn(); 441 | 442 | var list = fastn('list', { 443 | items: {foo:'bar'}, 444 | template: function(model){ 445 | t.equal(model.get('item'), 'bar'); 446 | t.equal(model.get('key'), 'foo'); 447 | } 448 | }); 449 | 450 | list.attach(); 451 | 452 | list.destroy(); 453 | 454 | }); 455 | 456 | test('warns on no template', function(t){ 457 | 458 | t.plan(1); 459 | 460 | var fastn = createFastn(); 461 | 462 | consoleWatch(function(getResults) { 463 | var list = fastn('list'); 464 | 465 | t.deepEqual(getResults(), {warn: ['No "template" function was set for this templater component']}) 466 | }); 467 | 468 | }); 469 | 470 | test('Lazy item templating', function(t){ 471 | 472 | t.plan(3); 473 | 474 | var fastn = createFastn(); 475 | 476 | var items = []; 477 | 478 | while(items.length < 100){ 479 | items.push(items.length); 480 | } 481 | 482 | var list = fastn('list', { 483 | insertionFrameTime: 100, 484 | items: items, 485 | template: function(model){ 486 | if(model.get('item') < 10){ 487 | var start = new Date(); 488 | while(Date.now() - start < 10){} 489 | } 490 | return fastn.binding('item'); 491 | } 492 | }); 493 | 494 | list.render(); 495 | 496 | document.body.appendChild(list.element); 497 | 498 | var expectedEventualText = items.join(''); 499 | 500 | t.equal(expectedEventualText.indexOf(document.body.textContent), 0); 501 | t.notEqual(document.body.textContent, expectedEventualText); 502 | 503 | setTimeout(function(){ 504 | 505 | t.equal(document.body.textContent, expectedEventualText); 506 | 507 | list.element.remove(); 508 | list.destroy(); 509 | }, 100); 510 | }); -------------------------------------------------------------------------------- /test/property.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | fastn = require('../index')({}), 3 | createBinding = fastn.binding, 4 | createProperty = fastn.property, 5 | Enti = require('enti'), 6 | EventEmitter = require('events'); 7 | 8 | test('simple property initialisation', function(t){ 9 | t.plan(3); 10 | 11 | var property = createProperty(); 12 | 13 | t.equal(property(), undefined); 14 | 15 | property('bar'); 16 | 17 | t.equal(property(), 'bar'); 18 | 19 | property.on('change', function(value){ 20 | t.equal(value, 'foo'); 21 | }); 22 | 23 | property('foo'); 24 | }); 25 | 26 | test('bound property', function(t){ 27 | t.plan(5); 28 | 29 | var property = createProperty(); 30 | 31 | var binding = createBinding('foo'); 32 | 33 | t.equal(property(), undefined, 'No initial value'); 34 | 35 | property('bar'); 36 | 37 | t.equal(property(), 'bar', 'bar set'); 38 | 39 | property.binding(binding); 40 | 41 | t.equal(property(), undefined, 'bar overridden by binding'); 42 | 43 | binding('baz'); 44 | 45 | t.equal(property(), 'baz', 'baz set via binding'); 46 | 47 | property.on('change', function(value){ 48 | t.equal(value, 'foo', 'property changed'); 49 | }); 50 | 51 | binding('foo'); 52 | }); 53 | 54 | test('bound property with model', function(t){ 55 | t.plan(3); 56 | 57 | var data = { 58 | foo: 'bar' 59 | }, 60 | model = new Enti(data), 61 | currentValue; 62 | 63 | var property = createProperty(); 64 | 65 | property.on('change', function(value){ 66 | t.equal(value, currentValue); 67 | }); 68 | 69 | var binding = createBinding('foo'); 70 | 71 | binding('baz'); 72 | currentValue = 'baz'; 73 | 74 | property.binding(binding); 75 | 76 | currentValue = 'bar'; 77 | property.attach(model); 78 | 79 | currentValue = 'foo'; 80 | model.set('foo', 'foo'); 81 | }); 82 | 83 | test('bound property with model and drill', function(t){ 84 | t.plan(1); 85 | 86 | var data = {}, 87 | model = new Enti(data); 88 | 89 | var property = createProperty(); 90 | 91 | var binding = createBinding('foo.bar'); 92 | 93 | binding.attach(model); 94 | 95 | property.binding(binding); 96 | 97 | property.on('change', function(value){ 98 | t.equal(value, 123); 99 | }); 100 | 101 | model.set('foo', {bar: 123}); 102 | }); 103 | 104 | test('cyclic value', function(t){ 105 | t.plan(1); 106 | 107 | var model = new Enti(); 108 | 109 | var property = createProperty(null, 'keys'); 110 | 111 | var binding = createBinding('.|*'); 112 | 113 | binding.attach(model); 114 | 115 | property.binding(binding); 116 | 117 | property.on('change', function(value){ 118 | t.equal(value, model.get('.')); 119 | }); 120 | 121 | model.set('self', model.get('.')); 122 | }); 123 | 124 | test('cyclic value with structure changes', function(t){ 125 | t.plan(1); 126 | 127 | var model = new Enti(); 128 | 129 | var property = createProperty(null, 'structure'); 130 | 131 | var binding = createBinding('.|*'); 132 | 133 | binding.attach(model); 134 | 135 | property.binding(binding); 136 | 137 | property.on('change', function(value){ 138 | t.equal(value, model.get('.')); 139 | }); 140 | 141 | model.set('self', model.get('.')); 142 | }); 143 | 144 | test('bound property to EventEmitter', function(t){ 145 | t.plan(5); 146 | 147 | var property = createProperty(); 148 | 149 | var observable = new EventEmitter(); 150 | 151 | t.equal(property(), undefined, 'No initial value'); 152 | 153 | property('bar'); 154 | 155 | t.equal(property(), 'bar', 'bar set'); 156 | 157 | property.binding(observable); 158 | 159 | t.equal(property(), undefined, 'bar overridden by observable'); 160 | 161 | observable.emit('change', 'baz'); 162 | 163 | t.equal(property(), 'baz', 'baz set via observable'); 164 | 165 | property.on('change', function(value){ 166 | t.equal(value, 'foo', 'property changed'); 167 | }); 168 | 169 | observable.emit('change', 'foo'); 170 | }); 171 | 172 | test('bound property to EventEmitter with custom attach', function(t){ 173 | t.plan(2); 174 | 175 | var property = createProperty(); 176 | 177 | function customObservable(path) { 178 | var observable = new EventEmitter(); 179 | observable.attach = function(data){ 180 | this.emit('change', data[path]) 181 | }; 182 | return observable; 183 | } 184 | 185 | var observable = customObservable('foo') 186 | 187 | property.binding(observable); 188 | 189 | t.equal(property(), undefined, 'no value'); 190 | 191 | property.attach({ 192 | foo: 'bar' 193 | }) 194 | 195 | t.equal(property(), 'bar', 'bar set via observable attach'); 196 | }); -------------------------------------------------------------------------------- /test/templater.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | consoleWatch = require('console-watch'), 3 | Enti = require('enti'), 4 | createFastn = require('./createFastn'); 5 | 6 | test('value data', function(t){ 7 | 8 | t.plan(1); 9 | 10 | var fastn = createFastn(); 11 | 12 | var template = fastn('templater', { 13 | data: {foo:'bar'}, 14 | template: function(model){ 15 | return fastn.binding('item.foo'); 16 | } 17 | }); 18 | 19 | template.render(); 20 | 21 | document.body.appendChild(template.element); 22 | 23 | t.equal(document.body.textContent, 'bar'); 24 | 25 | template.element.remove(); 26 | template.destroy(); 27 | 28 | }); 29 | 30 | 31 | test('bound data', function(t){ 32 | 33 | t.plan(1); 34 | 35 | var fastn = createFastn(); 36 | 37 | var template = fastn('templater', { 38 | data: fastn.binding('data|*'), 39 | template: function(model){ 40 | return fastn.binding('item.foo'); 41 | } 42 | }); 43 | 44 | template.attach({ 45 | data: { 46 | foo: 'bar' 47 | } 48 | }); 49 | template.render(); 50 | 51 | document.body.appendChild(template.element); 52 | 53 | t.equal(document.body.textContent, 'bar'); 54 | 55 | template.element.remove(); 56 | template.destroy(); 57 | 58 | }); 59 | 60 | 61 | test('bound data changing', function(t){ 62 | 63 | t.plan(2); 64 | 65 | var fastn = createFastn(); 66 | 67 | var template = fastn('templater', { 68 | data: fastn.binding('data|*'), 69 | template: function(model){ 70 | return fastn.binding('item.foo'); 71 | } 72 | }), 73 | model = new Enti({ 74 | data: { 75 | foo: 'bar' 76 | } 77 | }); 78 | 79 | template.attach(model); 80 | template.render(); 81 | 82 | document.body.appendChild(template.element); 83 | 84 | t.equal(document.body.textContent, 'bar'); 85 | 86 | model.set('data.foo', 'baz'); 87 | 88 | t.equal(document.body.textContent, 'baz'); 89 | 90 | template.element.remove(); 91 | template.destroy(); 92 | 93 | }); 94 | 95 | test('null data', function(t){ 96 | 97 | t.plan(1); 98 | 99 | var fastn = createFastn(); 100 | 101 | var template = fastn('templater', { 102 | data: null, 103 | template: function(model){} 104 | }); 105 | 106 | template.render(); 107 | 108 | document.body.appendChild(template.element); 109 | 110 | t.equal(document.body.textContent, ''); 111 | 112 | template.element.remove(); 113 | template.destroy(); 114 | 115 | }); 116 | 117 | test('undefined template', function(t){ 118 | 119 | t.plan(1); 120 | 121 | var fastn = createFastn(); 122 | 123 | var template = fastn('templater', { 124 | data: null, 125 | template: function(model){} 126 | }); 127 | 128 | template.render(); 129 | 130 | document.body.appendChild(template.element); 131 | 132 | t.equal(document.body.textContent, ''); 133 | 134 | template.element.remove(); 135 | template.destroy(); 136 | 137 | }); 138 | 139 | test('reuse template', function(t){ 140 | 141 | t.plan(1); 142 | 143 | var fastn = createFastn(); 144 | 145 | var template = fastn('templater', { 146 | data: 'foo', 147 | template: function(model, scope, lastTemplate){ 148 | if(lastTemplate){ 149 | return lastTemplate; 150 | } 151 | t.pass(); 152 | return fastn('text'); 153 | } 154 | }); 155 | 156 | template.render(); 157 | 158 | template.data('bar'); 159 | 160 | }); 161 | 162 | test('reuse template same element', function(t){ 163 | 164 | t.plan(3); 165 | 166 | var fastn = createFastn(); 167 | 168 | var template = fastn('templater', { 169 | data: 'foo', 170 | template: function(model, scope, lastTemplate){ 171 | if(lastTemplate){ 172 | return lastTemplate; 173 | } 174 | return fastn.binding('item'); 175 | } 176 | }); 177 | 178 | template.render(); 179 | 180 | document.body.appendChild(template.element); 181 | 182 | t.equal(document.body.textContent, 'foo'); 183 | 184 | var lastNode = document.body.childNodes[1]; 185 | 186 | // Don't re-render or re-insert the template if it is already rendered or inserted 187 | document.body.replaceChild = function(){ 188 | debugger 189 | t.fail(); 190 | }; 191 | 192 | template.data('bar'); 193 | 194 | t.equal(document.body.textContent, 'bar'); 195 | 196 | t.equal(lastNode, document.body.childNodes[1]); 197 | 198 | template.element.remove(); 199 | template.destroy(); 200 | 201 | }); 202 | 203 | test('reattach templater with attachTemplates = false', function(t){ 204 | 205 | t.plan(3); 206 | 207 | var fastn = createFastn(); 208 | 209 | var data = {foo: {bar: 1}}, 210 | template = fastn('templater', { 211 | data: fastn.binding('nothing'), 212 | attachTemplates: false, 213 | template: function(model, scope, lastTemplate){ 214 | return fastn.binding('bar'); 215 | } 216 | }) 217 | .attach(data) 218 | .binding('foo'); 219 | 220 | template.render(); 221 | 222 | document.body.appendChild(template.element); 223 | 224 | t.equal(document.body.textContent, '1'); 225 | 226 | fastn.Model.set(data, 'foo', { 227 | bar: 2 228 | }); 229 | 230 | t.equal(document.body.textContent, '2'); 231 | 232 | fastn.Model.set(data, 'foo', { 233 | bar: 3 234 | }); 235 | 236 | t.equal(document.body.textContent, '3'); 237 | 238 | template.element.remove(); 239 | template.destroy(); 240 | 241 | }); 242 | 243 | test('warns on no template', function(t){ 244 | 245 | t.plan(1); 246 | 247 | var fastn = createFastn(); 248 | 249 | consoleWatch(function(getResults) { 250 | var list = fastn('templater'); 251 | 252 | t.deepEqual(getResults(), {warn: ['No "template" function was set for this templater component']}) 253 | }); 254 | 255 | }); -------------------------------------------------------------------------------- /test/text.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'), 2 | Enti = require('enti'), 3 | createFastn = require('./createFastn'); 4 | 5 | test('value text', function(t){ 6 | 7 | t.plan(1); 8 | 9 | var fastn = createFastn(); 10 | 11 | var text = fastn('text', {text: 'foo'}); 12 | 13 | text.render(); 14 | 15 | document.body.appendChild(text.element); 16 | 17 | t.equal(document.body.textContent, 'foo'); 18 | 19 | text.element.remove(); 20 | text.destroy(); 21 | 22 | 23 | }); 24 | 25 | test('bound text', function(t){ 26 | 27 | t.plan(1); 28 | 29 | var fastn = createFastn(); 30 | 31 | var text = fastn('text', {text: fastn.binding('value')}); 32 | 33 | text.attach({ 34 | value: 'foo' 35 | }); 36 | text.render(); 37 | 38 | document.body.appendChild(text.element); 39 | 40 | t.equal(document.body.textContent, 'foo'); 41 | 42 | text.element.remove(); 43 | text.destroy(); 44 | 45 | 46 | }); 47 | 48 | test('bound text changing', function(t){ 49 | 50 | t.plan(2); 51 | 52 | var fastn = createFastn(); 53 | 54 | var text = fastn('text', {text: fastn.binding('value')}), 55 | model = new Enti({ 56 | value: 'foo' 57 | }); 58 | 59 | text.attach(model); 60 | text.render(); 61 | 62 | document.body.appendChild(text.element); 63 | 64 | t.equal(document.body.textContent, 'foo'); 65 | 66 | model.set('value', 'bar'); 67 | 68 | t.equal(document.body.textContent, 'bar'); 69 | 70 | text.element.remove(); 71 | text.destroy(); 72 | 73 | }); 74 | 75 | test('auto binding text', function(t){ 76 | 77 | t.plan(2); 78 | 79 | var fastn = createFastn(); 80 | 81 | var parent = fastn('span', fastn.binding('value')), 82 | model = new Enti({ 83 | value: 'foo' 84 | }); 85 | 86 | parent.attach(model); 87 | parent.render(); 88 | 89 | document.body.appendChild(parent.element); 90 | 91 | t.equal(document.body.textContent, 'foo'); 92 | 93 | model.set('value', 'bar'); 94 | 95 | t.equal(document.body.textContent, 'bar'); 96 | 97 | parent.element.remove(); 98 | parent.destroy(); 99 | 100 | }); 101 | 102 | test('undefined text', function(t){ 103 | t.plan(1); 104 | 105 | var fastn = createFastn(); 106 | 107 | var text = fastn('text', {text: undefined}); 108 | 109 | text.render(); 110 | 111 | document.body.appendChild(text.element); 112 | 113 | t.equal(document.body.textContent, ''); 114 | 115 | text.element.remove(); 116 | text.destroy(); 117 | }); 118 | 119 | 120 | test('auto text Date', function(t){ 121 | 122 | t.plan(1); 123 | 124 | var fastn = createFastn(); 125 | 126 | var date = new Date(), 127 | parent = fastn('span', date); 128 | 129 | parent.render(); 130 | 131 | document.body.appendChild(parent.element); 132 | 133 | t.equal(document.body.textContent, date.toString()); 134 | 135 | parent.element.remove(); 136 | parent.destroy(); 137 | 138 | }); 139 | 140 | 141 | test('clone text', function(t){ 142 | 143 | t.plan(2); 144 | 145 | var fastn = createFastn(); 146 | 147 | var parent = fastn('span', 'text'); 148 | 149 | parent.render(); 150 | 151 | document.body.appendChild(parent.element); 152 | 153 | t.equal(document.body.textContent, 'text'); 154 | 155 | parent.element.remove(); 156 | 157 | var newParent = parent.clone(); 158 | parent.destroy(); 159 | 160 | newParent.render(); 161 | 162 | document.body.appendChild(newParent.element); 163 | 164 | t.equal(document.body.textContent, 'text'); 165 | 166 | newParent.element.remove(); 167 | 168 | newParent.destroy(); 169 | 170 | }); 171 | 172 | 173 | test('clone text binding', function(t){ 174 | 175 | t.plan(2); 176 | 177 | var data = { 178 | foo: 'bar' 179 | }; 180 | 181 | var fastn = createFastn(); 182 | 183 | var binding = fastn.binding('foo').attach(data); 184 | 185 | var parent = fastn('span', binding); 186 | 187 | parent.render(); 188 | 189 | document.body.appendChild(parent.element); 190 | 191 | t.equal(document.body.textContent, 'bar'); 192 | 193 | parent.element.remove(); 194 | 195 | var newParent = parent.clone(); 196 | parent.destroy(); 197 | 198 | newParent.render(); 199 | 200 | document.body.appendChild(newParent.element); 201 | 202 | t.equal(document.body.textContent, 'bar'); 203 | 204 | newParent.element.remove(); 205 | 206 | newParent.destroy(); 207 | 208 | }); -------------------------------------------------------------------------------- /textComponent.js: -------------------------------------------------------------------------------- 1 | function updateText(){ 2 | if(!this.element){ 3 | return; 4 | } 5 | 6 | var value = this.text(); 7 | 8 | this.element.data = (value == null ? '' : value); 9 | } 10 | 11 | function autoRender(content){ 12 | this.element = document.createTextNode(content); 13 | } 14 | 15 | function autoText(text, fastn, content) { 16 | text.render = autoRender.bind(text, content); 17 | 18 | return text; 19 | } 20 | 21 | function render(){ 22 | this.element = this.createTextNode(this.text()); 23 | this.emit('render'); 24 | }; 25 | 26 | function textComponent(fastn, component, type, settings, children){ 27 | component.createTextNode = textComponent.createTextNode; 28 | component.render = render.bind(component); 29 | 30 | component.setProperty('text', fastn.property('', updateText.bind(component))); 31 | 32 | return component; 33 | } 34 | 35 | textComponent.createTextNode = function(text){ 36 | return document.createTextNode(text); 37 | }; 38 | 39 | module.exports = textComponent; 40 | --------------------------------------------------------------------------------