├── test ├── demo │ ├── test.css │ ├── error.css │ ├── service.css │ ├── partial.html │ ├── demo.css │ ├── test.html │ ├── error.html │ ├── service.html │ ├── index.html │ ├── editing.html │ └── demo.html ├── dummy-apps │ ├── 1 │ │ ├── error.css │ │ └── error.html │ ├── 2 │ │ ├── index.css │ │ ├── index.html │ │ └── there.html │ ├── 3 │ │ ├── there.css │ │ └── there.html │ ├── 4 │ │ ├── error.css │ │ └── error.html │ ├── 5 │ │ ├── error.css │ │ ├── test.css │ │ ├── index.css │ │ ├── error.html │ │ ├── index.html │ │ └── test.html │ ├── 6 │ │ ├── test.css │ │ ├── service1.css │ │ ├── service2.css │ │ ├── index.css │ │ ├── service2.html │ │ ├── index.html │ │ ├── service1.html │ │ └── test.html │ ├── 7 │ │ ├── test.css │ │ ├── index.css │ │ ├── index.html │ │ └── test.html │ ├── 8 │ │ ├── test.css │ │ ├── index.css │ │ ├── index.html │ │ └── test.html │ └── 9 │ │ ├── index.css │ │ ├── test.html │ │ └── index.html ├── compile.js └── index.html ├── .bowerrc ├── .jshintrc ├── .codeclimate.yml ├── packages ├── rebound-htmlbars │ ├── lib │ │ ├── hooks │ │ │ ├── destroyRenderNode.js │ │ │ ├── didCleanupTree.js │ │ │ ├── willCleanupTree.js │ │ │ ├── bindScope.js │ │ │ ├── cleanupRenderNode.js │ │ │ ├── getValue.js │ │ │ ├── createChildEnv.js │ │ │ ├── createChildScope.js │ │ │ ├── content.js │ │ │ ├── createFreshScope.js │ │ │ ├── createFreshEnv.js │ │ │ ├── subexpr.js │ │ │ ├── concat.js │ │ │ ├── linkRenderNode.js │ │ │ ├── get.js │ │ │ ├── invokeHelper.js │ │ │ ├── partial.js │ │ │ ├── attribute.js │ │ │ └── component.js │ │ ├── rebound-htmlbars.js │ │ ├── compile.js │ │ ├── hooks.js │ │ ├── lazy-value.js │ │ ├── helpers.js │ │ └── render.js │ └── test │ │ ├── rebound_helpers_register_test.js │ │ ├── rebound_helpers_on_test.js │ │ ├── rebound_helpers_partials_test.js │ │ ├── rebound_helpers_unless_test.js │ │ ├── rebound_helpers_if_test.js │ │ └── rebound_helpers_attribute_test.js ├── compiler.js ├── rebound-utils │ ├── lib │ │ ├── urls.js │ │ ├── ajax.js │ │ └── rebound-utils.js │ └── test │ │ └── rebound-utils.js ├── tests.js ├── rebound-compiler │ ├── lib │ │ ├── compile.js │ │ ├── precompile.js │ │ └── parser.js │ └── test │ │ ├── rebound_precompiler_test.js │ │ └── rebound_compiler_test.js ├── rebound-test │ └── lib │ │ └── test-helpers.js ├── rebound-router │ └── lib │ │ ├── service.js │ │ └── loader.js ├── runtime.js ├── rebound-data │ ├── test │ │ ├── rebound_collection_test.js │ │ └── rebound_model_test.js │ └── lib │ │ ├── rebound-data.js │ │ └── collection.js ├── property-compiler │ ├── lib │ │ └── property-compiler.js │ └── test │ │ └── property_compiler_test.js └── rebound-component │ ├── lib │ └── factory.js │ └── test │ └── rebound_services_test.js ├── .travis.yml ├── .gitignore ├── LICENSE ├── bower.json ├── package.json ├── tasks ├── release.js └── docco.js └── testem.json /test/demo/test.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/demo/error.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/demo/service.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/2/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/3/there.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/4/error.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/5/error.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/5/test.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/6/test.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/7/test.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/8/test.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/6/service1.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy-apps/6/service2.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /test/dummy-apps/1/error.css: -------------------------------------------------------------------------------- 1 | .foo{ 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/demo/partial.html: -------------------------------------------------------------------------------- 1 |
PARTIAL {{newTitle}} {{FOOBAR}}
2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "curly": false, 4 | "expr": true, 5 | "boss": true 6 | } 7 | -------------------------------------------------------------------------------- /test/demo/demo.css: -------------------------------------------------------------------------------- 1 | #rebound-logo{ 2 | width: 500px; 3 | margin: 20px auto -40px; 4 | display: block; 5 | } -------------------------------------------------------------------------------- /test/demo/test.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/demo/error.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | JavaScript: true 3 | exclude_paths: 4 | - "shims/*" 5 | - "test/*" 6 | - "wrap/*" 7 | - "Gulpfile.js" 8 | - "packages/property-compiler/lib/tokenizer.js" -------------------------------------------------------------------------------- /test/dummy-apps/1/error.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/5/index.css: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/6/index.css: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/7/index.css: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/8/index.css: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/9/index.css: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/4/error.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/5/error.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/7/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/8/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/2/there.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /test/dummy-apps/3/there.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/destroyRenderNode.js: -------------------------------------------------------------------------------- 1 | // ### Destroy-Render-Node Hook 2 | 3 | // Called when destroying a render node 4 | export default function destroyRenderNode(morph){ 5 | 6 | } 7 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/rebound-htmlbars.js: -------------------------------------------------------------------------------- 1 | import hooks from "rebound-htmlbars/hooks"; 2 | 3 | export var registerHelper = hooks.registerHelper; 4 | export var registerPartial = hooks.registerPartial; 5 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/didCleanupTree.js: -------------------------------------------------------------------------------- 1 | // ### Did-Cleanup-Tree Hook 2 | 3 | // Called after destroying any node tree 4 | export default function didCleanupTree(env, morph, destroySelf){ 5 | 6 | } 7 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/willCleanupTree.js: -------------------------------------------------------------------------------- 1 | // ### Will-Cleanup-Tree Hook 2 | 3 | // Called before destroying any node tree 4 | export default function willCleanupTree(env, morph, destroySelf){ 5 | 6 | } 7 | -------------------------------------------------------------------------------- /test/demo/service.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | -------------------------------------------------------------------------------- /test/dummy-apps/9/test.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/bindScope.js: -------------------------------------------------------------------------------- 1 | // ### Bind-Scope Hook 2 | 3 | // Make scope available on the environment object to allow hooks to cache streams on it. 4 | export default function bindScope(env, scope){ 5 | env.scope = scope; 6 | } -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/cleanupRenderNode.js: -------------------------------------------------------------------------------- 1 | // ### Cleanup-Render-Node Hook 2 | 3 | // Called before destroying any render node 4 | export default function cleanupRenderNode(morph){ 5 | // morph.lazyValue && morph.lazyValue.destroy(); 6 | } 7 | -------------------------------------------------------------------------------- /test/dummy-apps/6/service2.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /test/dummy-apps/9/index.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /packages/compiler.js: -------------------------------------------------------------------------------- 1 | 2 | // Rebound Compiletime 3 | // ---------------- 4 | 5 | // Import the runtime 6 | import * as Rebound from 'runtime'; 7 | 8 | // Load our **compiler** 9 | import compiler from "rebound-compiler/compile"; 10 | 11 | Rebound.compiler = compiler; 12 | 13 | export default Rebound; 14 | -------------------------------------------------------------------------------- /test/dummy-apps/7/test.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /test/dummy-apps/5/test.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 15 | 16 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/getValue.js: -------------------------------------------------------------------------------- 1 | // ### Get Value Hook 2 | 3 | // The getValue hook retreives the value of the passed in referance. 4 | // It will return the propper value regardless of if the referance passed is the 5 | // value itself, or a LazyValue. 6 | export default function getValue(referance){ 7 | return (referance && referance.isLazyValue) ? referance.value : referance; 8 | } 9 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/createChildEnv.js: -------------------------------------------------------------------------------- 1 | // ### Create-Child-Environment Hook 2 | 3 | // Create an environment object that will inherit everything from its parent 4 | // environment until written over with a local variable. 5 | export default function createChildEnv(parent){ 6 | var env = Object.create(parent); 7 | env.helpers = Object.create(parent.helpers); 8 | return env; 9 | } 10 | -------------------------------------------------------------------------------- /test/dummy-apps/6/index.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /test/dummy-apps/6/service1.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/compile.js: -------------------------------------------------------------------------------- 1 | import { compileSpec } from "htmlbars"; 2 | 3 | // Return an executable function (object) version of the compiled template string 4 | export function compile(string){ 5 | return new Function("return " + compileSpec(string))(); // jshint ignore:line 6 | } 7 | 8 | // Return a precompiled (string) version of the compiled template string 9 | export function precompile(string){ 10 | return compileSpec(string); 11 | } -------------------------------------------------------------------------------- /packages/rebound-utils/lib/urls.js: -------------------------------------------------------------------------------- 1 | import qs from "qs"; 2 | 3 | var QS_STRINGIFY_OPTS = { 4 | allowDots: true, 5 | encode: false, 6 | delimiter: '&' 7 | }; 8 | 9 | var QS_PARSE_OPTS = { 10 | allowDots: true, 11 | delimiter: /[;,&]/ 12 | }; 13 | 14 | var query = { 15 | stringify(str){ return qs.stringify(str, QS_STRINGIFY_OPTS); }, 16 | parse(obj){ return qs.parse(obj, QS_PARSE_OPTS); } 17 | }; 18 | 19 | export { query as query }; 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.1" 4 | sudo: false 5 | addons: 6 | sauce_connect: 7 | username: reboundjs 8 | access_key: 07875532-956f-448b-a77d-1520ac359f66 9 | before_script: 10 | - "[[ $SUITE = saucelabs ]] && npm install -g saucie || true" 11 | - "[[ $SUITE = saucelabs ]] && npm install -g testem || true" 12 | - "npm run build" 13 | env: 14 | - SUITE=phantomjs 15 | - SUITE=saucelabs 16 | script: "[[ $SUITE = phantomjs ]] && npm test || testem ci" 17 | -------------------------------------------------------------------------------- /test/dummy-apps/6/test.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 17 | 18 | -------------------------------------------------------------------------------- /test/dummy-apps/8/test.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/createChildScope.js: -------------------------------------------------------------------------------- 1 | // ### Create-Child-Scope Hook 2 | 3 | // Create a scope object that will inherit everything from its parent 4 | // scope until written over with a local variable. 5 | export default function createChildScope(parent) { 6 | var scope = Object.create(parent); 7 | scope.level = parent.level + 1; 8 | scope.locals = Object.create(parent.locals); 9 | scope.localPresent = Object.create(parent.localPresent); 10 | scope.streams = Object.create(parent.streams); 11 | scope.blocks = Object.create(parent.blocks); 12 | return scope; 13 | } 14 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/content.js: -------------------------------------------------------------------------------- 1 | // ### Content Hook 2 | 3 | // Content Hook 4 | export default function content(morph, env, context, path, lazyValue){ 5 | var el = morph.contextualElement; 6 | 7 | // Two way databinding for textareas 8 | if(el.tagName === 'TEXTAREA'){ 9 | lazyValue.onNotify(function updateTextarea(lazyValue){ 10 | el.value = lazyValue.value; 11 | }); 12 | $(el).on('change keyup', function updateTextareaLazyValue(event){ 13 | lazyValue.set(lazyValue.path, this.value); 14 | }); 15 | } 16 | 17 | morph.lazyValue = lazyValue; 18 | 19 | return lazyValue.value; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/createFreshScope.js: -------------------------------------------------------------------------------- 1 | // ### Create-Fresh-Scope Hook 2 | 3 | // Rebound's default scope object. 4 | // The scope object is propagated down each block expression or render call and 5 | // augmented with local variables as it goes. LazyValues are cached as streams 6 | // here as well. Because `in` checks have unpredictable performance, keep a 7 | // separate dictionary to track whether a local was bound. 8 | export default function createFreshScope() { 9 | return { 10 | level: 1, 11 | self: null, 12 | locals: {}, 13 | localPresent: {}, 14 | streams: {}, 15 | blocks: {} 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /test/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/createFreshEnv.js: -------------------------------------------------------------------------------- 1 | // ### Create-Fresh-Environment Hook 2 | 3 | // Rebound's default environment 4 | // The application environment is propagated down each render call and 5 | // augmented with helpers as it goes 6 | import { default as _DOMHelper } from "dom-helper"; 7 | import helpers from "rebound-htmlbars/helpers"; 8 | 9 | var DOMHelper = _DOMHelper.default || _DOMHelper; // Fix for stupid Babel imports 10 | 11 | export default function createFreshEnv(){ 12 | return { 13 | isReboundEnv: true, 14 | cid: _.uniqueId('env'), 15 | root: null, 16 | helpers: helpers, 17 | hooks: this, 18 | dom: new DOMHelper(), 19 | revalidateQueue: {}, 20 | observers: {}, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Tmp build directories 23 | tmp 24 | amd 25 | dist 26 | test/demo/templates 27 | test/dummy-apps/*.js 28 | test/dummy-apps/**/*.js 29 | 30 | test/rebound.tests.js 31 | docs 32 | 33 | # Dependency directory 34 | # Deployed apps should consider commenting this line out: 35 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 36 | node_modules 37 | bower_components 38 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/subexpr.js: -------------------------------------------------------------------------------- 1 | // ### Subexpr Hook 2 | 3 | // The `subexpr` hook creates a LazyValue for a nexted expression so it may be 4 | // used as a single data point in its parent expression. For example: 5 | // ``` 6 | // {{#if (equal (add 1 2) 3)}}True!{{/if}} 7 | // ``` 8 | // The `if` block expression contains a subexpression that is the evalued value 9 | // of the `equal` helper, which in turn contains a subexpression that is the 10 | // evalued value of the `add` helper. Each subexpression is represented internally 11 | // by a single LazyValue that notifies its subscribers when it changes. 12 | export default function subexpr(env, scope, helperName, params, hash) { 13 | var helper = this.lookupHelper(helperName, env); 14 | 15 | // Return the apropreate LazyValue for this subexpression type. 16 | if (helper) { 17 | return this.invokeHelper(null, env, scope, null, params, hash, helper, {}, undefined); 18 | } 19 | 20 | return this.get(env, scope, helperName); 21 | 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Adam Miller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/demo/editing.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 37 | 38 | -------------------------------------------------------------------------------- /packages/tests.js: -------------------------------------------------------------------------------- 1 | import utils from "rebound-utils/test/rebound-utils"; 2 | 3 | import propertyCompiler from "property-compiler/test/property_compiler_test"; 4 | 5 | import compiler from "rebound-compiler/test/rebound_compiler_test"; 6 | import precompiler from "rebound-compiler/test/rebound_precompiler_test"; 7 | 8 | import component from "rebound-component/test/rebound_component_test"; 9 | import services from "rebound-component/test/rebound_services_test"; 10 | 11 | import helpers_attribute from "rebound-htmlbars/test/rebound_helpers_attribute_test"; 12 | import helpers_each from "rebound-htmlbars/test/rebound_helpers_each_test"; 13 | import helpers_if from "rebound-htmlbars/test/rebound_helpers_if_test"; 14 | import helpers_on from "rebound-htmlbars/test/rebound_helpers_on_test"; 15 | import helpers_partials from "rebound-htmlbars/test/rebound_helpers_partials_test"; 16 | import helpers_register from "rebound-htmlbars/test/rebound_helpers_register_test"; 17 | import helpers_unless from "rebound-htmlbars/test/rebound_helpers_unless_test"; 18 | 19 | import model from "rebound-data/test/rebound_model_test"; 20 | import collection from "rebound-data/test/rebound_collection_test"; 21 | import computedProperty from "rebound-data/test/rebound_computed_property_test"; 22 | 23 | import router from "rebound-router/test/rebound_router_test"; 24 | 25 | QUnit.start(); 26 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/concat.js: -------------------------------------------------------------------------------- 1 | // ### Concat Hook 2 | 3 | // The `concat` hook creates a LazyValue for adjacent expressions so they may be 4 | // used as a single data point in its parent expression. For example: 5 | // ``` 6 | //
7 | // ``` 8 | // The div's attribute expression is passed a concat LazyValue that alerts its 9 | // subscribers whenever any of its dynamic values change. 10 | 11 | var CONCAT_CACHE = {}; 12 | 13 | import LazyValue from "rebound-htmlbars/lazy-value"; 14 | 15 | export default function concat(env, params){ 16 | 17 | // If the concat expression only contains a single value, return it. 18 | if(params.length === 1){ return params[0]; } 19 | 20 | // Each concat LazyValue is unique to its inputs. Compute it's unique name. 21 | var name = "concat: "; 22 | _.each(params, function(param, index){ 23 | name += `${((param && param.isLazyValue) ? param.cid : param )}`; 24 | }); 25 | 26 | // Check the streams cache and return if this LazyValue has already been made 27 | if(CONCAT_CACHE[name]){ return CONCAT_CACHE[name]; } 28 | 29 | // Create a lazyvalue that returns the concatted values of all input params 30 | // Add it to the streams cache and return 31 | return CONCAT_CACHE[name] = new LazyValue(function(params) { 32 | return params.join(''); 33 | }, { 34 | context: params[0].context, 35 | path: name, 36 | params: params 37 | }); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/linkRenderNode.js: -------------------------------------------------------------------------------- 1 | // ### Link-Render-Node Hook 2 | 3 | // Called on first creation of any expressions that interact directly with the DOM. 4 | // Whenever it is notified of any changes to it's dependant values, mark the node 5 | // as dirty and add it to the environment's revalidation queue to be rerendered 6 | // during the next animation frame. 7 | export default function linkRenderNode(renderNode, env, scope, path, params, hash){ 8 | 9 | function rerender(path, node, lazyValue, env){ 10 | lazyValue.onNotify(function(){ 11 | node.isDirty = true; 12 | env.template && (env.revalidateQueue[env.template.uid] = env.template); 13 | }); 14 | } 15 | 16 | // Save the path on our render node for easier debugging 17 | renderNode.path = path; 18 | 19 | // For every parameter or hash value passed to this render node, if it is a data 20 | // stream, subscribe to notifications from it and when notified of a change, 21 | // mark the node as dirty and queue it up for revalidation. 22 | if (params && params.length) { 23 | for (var i = 0; i < params.length; i++) { 24 | if(params[i].isLazyValue){ 25 | rerender(path, renderNode, params[i], env); 26 | } 27 | } 28 | } 29 | if (hash) { 30 | for (var key in hash) { 31 | if(hash.hasOwnProperty(key) && hash[key].isLazyValue){ 32 | rerender(path, renderNode, hash[key], env); 33 | } 34 | } 35 | } 36 | return 1; 37 | } 38 | -------------------------------------------------------------------------------- /test/compile.js: -------------------------------------------------------------------------------- 1 | var through = require('through2'); 2 | var path = require('path'); 3 | var gutil = require('gulp-util'); 4 | var rebound = require('../dist/cjs/rebound-compiler/precompile').default; 5 | var PluginError = gutil.PluginError; 6 | 7 | // Consts 8 | const PLUGIN_NAME = 'gulp-rebound'; 9 | 10 | function prefixStream(prefixText) { 11 | var stream = through(); 12 | stream.write(prefixText); 13 | return stream; 14 | } 15 | 16 | // Plugin level function(dealing with files) 17 | function gulpRebound(options) { 18 | 19 | options || (options = {}); 20 | 21 | // Creating a stream through which each file will pass 22 | return through.obj(function(file, enc, cb) { 23 | // return empty file 24 | if (file.isNull()) return cb(null, file); 25 | 26 | if (file.isStream()) { 27 | throw new PluginError(PLUGIN_NAME, "Gulp Rebound doesn't handle streams!"); 28 | } 29 | 30 | // Compile 31 | try { 32 | 33 | file.contents = new Buffer(rebound(file.contents.toString(enc), { 34 | name: options.root + '/' + path.parse(file.relative).name, // file.stem not reliable 35 | }).src, enc); 36 | 37 | gutil.log(gutil.colors.green('File ' + file.relative + ' compiled')); 38 | file.path = file.path.replace('.html', '.js'); 39 | } catch(err){ 40 | gutil.log(gutil.colors.red('Error in ' + file.relative)); 41 | gutil.log(err); 42 | } 43 | 44 | cb(null, file); 45 | 46 | }); 47 | 48 | } 49 | 50 | // Exporting the plugin main function 51 | module.exports = gulpRebound; 52 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/get.js: -------------------------------------------------------------------------------- 1 | // ### Get Hook 2 | 3 | // The get hook streams a property at a named path from a given scope. It returns 4 | // a `LazyValue` that other code can subscribe to and be alerted when values change. 5 | import $ from "rebound-utils/rebound-utils"; 6 | import LazyValue from "rebound-htmlbars/lazy-value"; 7 | 8 | export default function get(env, scope, path){ 9 | var context = scope.self; 10 | 11 | // The special word `this` should referance empty string 12 | if(path === 'this'){ path = ''; } 13 | 14 | // If this path referances a block param, use that as the context instead. 15 | var rest = $.splitPath(path); 16 | var key = rest.shift(); 17 | if(scope.localPresent[key]){ 18 | context = scope.locals[key]; 19 | path = rest.join('.'); 20 | } 21 | 22 | // If this value is not a local value, and there is a stream present 23 | // If this value is a local, but not at this scope layer, and there is 24 | if(scope.streams[path] && 25 | (!scope.streams[path].layer && !scope.localPresent[key] || scope.streams[path].layer === scope.localPresent[key])){ return scope.streams[path]; } 26 | 27 | // Given a context and a path, create a LazyValue object that returns 28 | // the value of object at path and add an observer to the context at path. 29 | return scope.streams[path] = new LazyValue(function() { 30 | return this.context.get(this.path, {isPath: true}); 31 | }, { 32 | context: context, 33 | path: path, 34 | layer: scope.localPresent[key] 35 | }).addObserver(path, context, env); 36 | } 37 | -------------------------------------------------------------------------------- /packages/rebound-compiler/lib/compile.js: -------------------------------------------------------------------------------- 1 | // Rebound Compiler 2 | // ---------------- 3 | 4 | import parse from "rebound-compiler/parser"; 5 | import { compile as compileTemplate } from "rebound-htmlbars/compile"; 6 | import { registerPartial } from "rebound-htmlbars/rebound-htmlbars"; 7 | import render from "rebound-htmlbars/render"; 8 | import Component from "rebound-component/factory"; 9 | import loader from "rebound-router/loader"; 10 | 11 | function compile(str, options={}){ 12 | /* jshint evil: true */ 13 | 14 | // Parse the component 15 | var defs = parse(str, options); 16 | 17 | // Compile our template 18 | defs.template = compileTemplate(defs.template); 19 | 20 | // For client side rendered templates, put the render function directly on the 21 | // template result for convenience. To sue templates rendered server side will 22 | // consumers will have to invoke the view layer's render function themselves. 23 | defs.template.render = function(el, data, options){ 24 | return render(el, this, data, options); 25 | }; 26 | 27 | // Fetch any dependancies 28 | loader.load(defs.deps); 29 | 30 | // If this is a partial, register the partial 31 | if(defs.isPartial){ 32 | if(options.name){ registerPartial(options.name, defs.template); } 33 | return defs.template; 34 | } 35 | 36 | // If this is a component, register the component 37 | else{ 38 | return Component.registerComponent(defs.name, { 39 | prototype: new Function("return " + defs.script)(), 40 | template: defs.template, 41 | stylesheet: defs.stylesheet 42 | }); 43 | } 44 | } 45 | 46 | export default { compile }; 47 | -------------------------------------------------------------------------------- /packages/rebound-compiler/lib/precompile.js: -------------------------------------------------------------------------------- 1 | // Rebound Pre-Compiler 2 | // ---------------- 3 | 4 | import parse from "./parser"; 5 | import { precompile as compileTemplate } from "../rebound-htmlbars/compile"; 6 | 7 | export default function precompile(str, options={}){ 8 | 9 | if( !str || str.length === 0 ){ 10 | return console.error('No template provided!'); 11 | } 12 | 13 | // Ensure baseDest exists 14 | options.baseDest || (options.baseDest = ''); 15 | 16 | var template; 17 | str = parse(str, options); 18 | 19 | // Compile 20 | str.template = '' + compileTemplate(str.template); 21 | 22 | // If is a partial 23 | if (str.isPartial) { 24 | template = [ 25 | "(function(R){", 26 | " R.router._loadDeps([ " + (str.deps.length ? `"${options.baseDest}` + str.deps.join(`", "${options.baseDest}`) + '"' : '') + " ]);", 27 | " R.registerPartial(\"" + str.name + "\", " + str.template + ");", 28 | "})(window.Rebound);"].join('\n'); 29 | } 30 | // Else, is a component 31 | else { 32 | template = [ 33 | "(function(R){", 34 | " R.router._loadDeps([ " + (str.deps.length ? `"${options.baseDest}` + str.deps.join(`", "${options.baseDest}`) + '"' : '') + " ]);", 35 | " document.currentScript.setAttribute(\"data-name\", \"" + str.name + "\");", 36 | " return R.registerComponent(\"" + str.name + "\", {", 37 | " prototype: " + str.script + ",", 38 | " template: " + str.template + ",", 39 | " stylesheet: \"" + str.stylesheet + "\"", 40 | " });", 41 | "})(window.Rebound);"].join('\n'); 42 | } 43 | return {src: template, deps: str.deps}; 44 | } 45 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rebound QUnit Tests 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reboundjs", 3 | "version": "0.0.4", 4 | "homepage": "https://github.com/epicmiller/rebound", 5 | "authors": [ 6 | "Adam Miller " 7 | ], 8 | "description": "Automatic data-binding for Backbone. Whats not to love?", 9 | "main": [ 10 | "dist/rebound.runtime.js" 11 | ], 12 | "moduleType": [ 13 | "amd", 14 | "es6", 15 | "globals" 16 | ], 17 | "keywords": [ 18 | "data-binding", 19 | "htmlbars", 20 | "backbone", 21 | "view", 22 | "javascript" 23 | ], 24 | "license": "MIT", 25 | "ignore": [ 26 | "bin", 27 | "node_modules", 28 | "bower_components", 29 | "packages", 30 | "shims", 31 | "test", 32 | "wrap" 33 | ], 34 | "devDependencies": { 35 | "jquery": "2.1.1", 36 | "underscore": "1.8.0", 37 | "lodash": "~2.4.1", 38 | "webcomponentsjs": "0.6.1", 39 | "pretender": "0.6.0", 40 | "backbone": "1.2.3", 41 | "classList": "https://github.com/eligrey/classList.js.git", 42 | "requestAnimationFrame": "https://gist.github.com/1579671.git", 43 | "currentScript": "https://github.com/epicmiller/currentScript-polyfill.git", 44 | "document-register-element": "0.5.4", 45 | "exoskeleton": "https://github.com/paulmillr/exoskeleton.git", 46 | "requirejs": "https://github.com/jrburke/requirejs.git#19a98167858db65342a269ba76f06e3be152210e", 47 | "almond": "0.2.9", 48 | "acorn": "~0.6.0", 49 | "qunit": "~1.20.0", 50 | "todomvc-common": "0.1.9", 51 | "html5shiv": "~3.7.2", 52 | "matchesSelector": "https://gist.github.com/3062955.git", 53 | "promise-polyfill": "~2.1.0", 54 | "console-polyfill": "https://github.com/paulmillr/console-polyfill.git" 55 | }, 56 | "dependencies": { 57 | "setimmediate": "~1.0.1" 58 | }, 59 | "resolutions": { 60 | "jquery": "~2.1.4" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/rebound-test/lib/test-helpers.js: -------------------------------------------------------------------------------- 1 | // Used to test a specific component. 2 | // Marks constructor as in "test" mode so dom updates are syncrynous. 3 | // TODO: Turn this into an override of Component's _onchange function. 4 | window.Rebound.test = function(component, callback){ 5 | require([component], function(constructor){ 6 | constructor.prototype.testing = true; 7 | callback.call(this, constructor); 8 | }); 9 | }; 10 | 11 | // Insert our test viewer onto the page 12 | document.addEventListener("DOMContentLoaded", function() { 13 | var container = document.createElement('main'); 14 | container.style.width = '100%'; 15 | container.style.height = '100%'; 16 | container.style.overflow = 'auto'; 17 | container.style.position = 'fixed'; 18 | container.style.background = 'white'; 19 | container.style.border = '1px solid #333'; 20 | container.style.bottom = '0px'; 21 | container.style.right = '0'; 22 | container.style.boxSizing = 'border-box'; 23 | container.style.transform = 'scale(.4)'; 24 | container.style.transformOrigin = '100% 100%'; 25 | 26 | // TODO: Allow the user to provide the content that goes into the test viewer. 27 | var nav = document.createElement('nav'); 28 | var content = document.createElement('content'); 29 | container.appendChild(nav); 30 | container.appendChild(content); 31 | 32 | document.body.appendChild(container); 33 | }); 34 | 35 | // Route to the specified url 36 | var visit = function visit(url){ 37 | 38 | }; 39 | 40 | var click = function click(selector){ 41 | 42 | }; 43 | 44 | // Fill in the selected input with the text. Should trigger appropreate keyboard events 45 | var fillIn = function fillIn(selector, text){ 46 | 47 | }; 48 | 49 | // Triggers an event on an element 50 | var trigger = function trigger(url){ 51 | 52 | }; 53 | 54 | // Calls a synchronous callback 55 | var then = function then(callback){ 56 | 57 | }; -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/invokeHelper.js: -------------------------------------------------------------------------------- 1 | // ### Invoke-Helper Hook 2 | 3 | // The `invokeHelper` hook streams a the result of a helper function. It returns 4 | // a `LazyValue` that other code can subscribe to and be alerted when values change. 5 | import $ from "rebound-utils/rebound-utils"; 6 | import LazyValue from "rebound-htmlbars/lazy-value"; 7 | 8 | export default function invokeHelper(morph, env, scope, visitor, params, hash, helper, templates, context){ 9 | 10 | // If this is not a valid helper, log an error and return an empty string value. 11 | if(!_.isFunction(helper)){ 12 | console.error('Invalid helper!', helper); 13 | return {value: ''}; 14 | } 15 | 16 | // Each helper LazyValue is unique to its inputs. Compute it's unique name. 17 | var name = `${helper.name}:`; 18 | _.each(params, function(param, index){ 19 | name += ` ${(param && param.isLazyValue) ? param.cid : param}`; 20 | }); 21 | _.each(hash, function(hash, key){ 22 | name += ` ${key}=${hash.cid}`; 23 | }); 24 | 25 | // Check the stream cache for this LazyValue, return it if it exists. 26 | if(scope.streams[name]){ return scope.streams[name]; } 27 | 28 | // Create a LazyValue that returns the value of our evaluated helper. 29 | var lazyValue = new LazyValue(function(params, hash){ 30 | return helper.call((context || {}), params, hash, templates, env); 31 | }, { 32 | path: name, 33 | params: params, 34 | hash: hash 35 | }); 36 | 37 | // If this is not a block or element helper, cache the new lazyValue. 38 | // Only block helpers will have a context set passed. Non-element helpers will 39 | // have the morph set. Block and morph helpers have re-rendered dom that must 40 | // be fresh in the LazyValue's closure each run. 41 | if(!context && morph){ scope.streams[name] = lazyValue; } 42 | 43 | // Seed the cache and return the new LazyValue 44 | lazyValue.value; 45 | return lazyValue; 46 | } 47 | -------------------------------------------------------------------------------- /packages/rebound-router/lib/service.js: -------------------------------------------------------------------------------- 1 | import { $, REBOUND_SYMBOL } from "rebound-utils/rebound-utils"; 2 | 3 | // Services cache of all installed services 4 | const SERVICES = {}; 5 | 6 | // Services keep track of their consumers. LazyComponent are placeholders 7 | // for services that haven't loaded yet. A LazyComponent mimics the api of a 8 | // real service/component (they are the same), and when the service finally 9 | // loads, its ```hydrate``` method is called. All consumers of the service will 10 | // have the now fully loaded service set, the LazyService will transfer all of 11 | // its consumers over to the fully loaded service, and then commit seppiku, 12 | // destroying itself. 13 | function ServiceLoader(type, options){ 14 | var loadCallbacks = []; 15 | this.name = type; 16 | this.cid = $.uniqueId('ServiceLoader'); 17 | this.isHydrated = false; 18 | this.isComponent = true; 19 | this.isModel = true; 20 | this.isLazyComponent = true; 21 | this.attributes = {}; 22 | this.consumers = []; 23 | this.set = this.on = this.off = function(){ 24 | return 1; 25 | }; 26 | this.get = function(path){ 27 | return (path) ? undefined : this; 28 | }; 29 | this.hydrate = function(service){ 30 | SERVICES[this.name] = service; 31 | this._component = service; 32 | _.each(this.consumers, function(consumer){ 33 | var component = consumer.component, 34 | key = consumer.key; 35 | if(component.attributes && component.set){ component.set(key, service); } 36 | if(component.services){ component.services[key] = service; } 37 | if(component.defaults){ component.defaults[key] = service; } 38 | }); 39 | service.consumers = this.consumers; 40 | 41 | // Call all of our onLoad callbacks 42 | _.each(loadCallbacks, (cb)=>{ cb(service); }); 43 | delete this.loadCallbacks; 44 | }; 45 | this.onLoad = function(cb){ 46 | loadCallbacks.push(cb); 47 | }; 48 | } 49 | 50 | export { ServiceLoader as ServiceLoader }; 51 | export { SERVICES as SERVICES }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reboundjs", 3 | "version": "0.3.4", 4 | "description": "Automatic data binding for Backbone using HTMLBars.", 5 | "main": "dist/cjs/rebound-compiler/precompile.js", 6 | "scripts": { 7 | "test": "TEST_ENV=true gulp test", 8 | "start": "gulp default", 9 | "build": "gulp build", 10 | "prepublish": "bower install ", 11 | "postpublish": "gulp release" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/epicmiller/rebound.git" 16 | }, 17 | "author": "Adam Miller - Pages, LLC.", 18 | "license": "MIT", 19 | "readmeFilename": "README.md", 20 | "files": [ 21 | "dist", 22 | "README.md", 23 | "package.json" 24 | ], 25 | "dependencies": { 26 | "gulp-filter": "^3.0.1", 27 | "gulp-rollup": "^1.6.0", 28 | "htmlbars": "0.14.11", 29 | "rollup-plugin-commonjs": "^2.1.0", 30 | "rollup-plugin-npm": "^1.2.0" 31 | }, 32 | "devDependencies": { 33 | "acorn": "^2.6.4", 34 | "babel-plugin-add-module-exports": "^0.1.2", 35 | "babel-plugin-transform-es2015-modules-amd": "^6.3.13", 36 | "babel-plugin-transform-es2015-modules-commonjs": "^6.3.16", 37 | "babel-plugin-transform-es2015-modules-umd": "^6.3.13", 38 | "babel-preset-es2015": "^6.3.13", 39 | "backbone": "^1.2.3", 40 | "bower": "1.6.5", 41 | "browserify": "^12.0.1", 42 | "compression": "^1.6.0", 43 | "del": "1.1.1", 44 | "event-stream": "3.2.1", 45 | "gulp": "3.9.0", 46 | "gulp-babel": "^6.1.1", 47 | "gulp-concat": "2.4.3", 48 | "gulp-connect": "2.2.0", 49 | "gulp-docco": "0.0.4", 50 | "gulp-git": "1.0.0", 51 | "gulp-jshint": "1.11.2", 52 | "gulp-merge": "^0.1.1", 53 | "gulp-rebound": "1.0.0", 54 | "gulp-rename": "1.2.0", 55 | "gulp-replace": "0.5.3", 56 | "gulp-sourcemaps": "^1.6.0", 57 | "gulp-uglify": "^1.4.2", 58 | "gulp-util": "^3.0.7", 59 | "jshint-stylish": "^2.0.1", 60 | "mkdirp": "0.5.0", 61 | "node-qunit-phantomjs": "jonkemp/node-qunit-phantomjs#b87f14964839b3f3b363582dce34d9f4c59fd9c5", 62 | "qs": "5.0.0", 63 | "through2": "^2.0.0", 64 | "vinyl-buffer": "^1.0.0", 65 | "vinyl-source-stream": "^1.1.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/partial.js: -------------------------------------------------------------------------------- 1 | import $ from "rebound-utils/rebound-utils"; 2 | import loader from "rebound-router/loader"; 3 | import LazyValue from "rebound-htmlbars/lazy-value"; 4 | 5 | var PARTIALS = {}; 6 | 7 | export function registerPartial(name, template){ 8 | if(template && _.isString(name)){ 9 | 10 | // If this partial has a callback list associated with its name, call all of 11 | // the callbacks before registering the partial. 12 | if(Array.isArray(PARTIALS[name])){ 13 | PARTIALS[name].forEach(function(cb) { cb(template); }); 14 | } 15 | 16 | // Save the partial template in our cache and return it 17 | loader.register('/'+name+'.js'); 18 | return PARTIALS[name] = template; 19 | } 20 | } 21 | 22 | export default function partial(renderNode, env, scope, path){ 23 | 24 | // If no path is passed, yell 25 | if(!path){ console.error('Partial helper must be passed a path!'); } 26 | 27 | // Resolve our path value 28 | path = path.isLazyValue ? path.value : path; 29 | 30 | // Create new child scope for partial 31 | scope = this.createChildScope(scope); 32 | 33 | var render = this.buildRenderResult; 34 | 35 | // Because of how htmlbars works with re-renders, we need a contextual element 36 | // for our partial that will not disappear on it when lazy partials are loaded. 37 | // We use a `` element for this. 38 | var node = document.createElement('rebound-partial'); 39 | node.setAttribute('path', path); 40 | 41 | // If a partial is registered with this path name, render it 42 | if(PARTIALS[path] && !Array.isArray(PARTIALS[path])){ 43 | node.appendChild(render(PARTIALS[path], env, scope, { contextualElement: renderNode}).fragment); 44 | } 45 | 46 | // If this partial is not yet registered, add it to a callback list to be called 47 | // when registered. When registered, replace the dummy node we created with the 48 | // rendered partial template. 49 | else{ 50 | PARTIALS[path] || (PARTIALS[path] = []); 51 | PARTIALS[path].push(function partialCallback(template){ 52 | node.appendChild(render(template, env, scope, { contextualElement: renderNode}).fragment, node); 53 | }); 54 | } 55 | 56 | return node; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tasks/release.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var del = require('del'); 3 | var git = require('gulp-git'); 4 | var pjson = require('../package.json'); 5 | var mkdirp = require('mkdirp'); 6 | var replace = require('gulp-replace'); 7 | 8 | /******************************************************************************* 9 | 10 | Release Tasks: 11 | 12 | gulp release is run on postpublish and automatically pushes the contents of /dist 13 | to https://github.com/epicmiller/rebound-dist for consumption by bower. 14 | 15 | *******************************************************************************/ 16 | gulp.task('cleanrelease', ['build'], function(cb){ 17 | return del(['tmp'], cb); 18 | }); 19 | 20 | // Clone a remote repo 21 | gulp.task('clone', ['cleanrelease'], function(cb){ 22 | console.log('Cloning rebound-dist to /tmp'); 23 | mkdirp.sync('./tmp'); 24 | git.clone('https://github.com/reboundjs/rebound-dist.git', {cwd: './tmp'}, cb); 25 | }); 26 | 27 | gulp.task('release-copy', ['clone'], function(cb){ 28 | return gulp.src('dist/**/*') 29 | .pipe(gulp.dest('tmp/rebound-dist')); 30 | }); 31 | 32 | gulp.task('bump-version', ['release-copy'], function(cb){ 33 | return gulp.src(['tmp/rebound-dist/bower.json']) 34 | .pipe(replace(/(.\s"version": ")[^"]*(")/g, '$1'+pjson.version+'$2')) 35 | .pipe(gulp.dest('tmp/rebound-dist')); 36 | }); 37 | 38 | gulp.task('add', ['bump-version'], function(){ 39 | console.log('Adding Rebound /dist directory to rebound-dist'); 40 | process.chdir('tmp/rebound-dist'); 41 | return gulp.src('./*') 42 | .pipe(git.add({args: '-A'})); 43 | }); 44 | 45 | gulp.task('commit', ['add'], function(cb){ 46 | console.log('Committing Rebound v' + pjson.version); 47 | return gulp.src('./*') 48 | .pipe(git.commit('Rebound version v'+pjson.version)); 49 | }); 50 | 51 | // Tag the repo with a version 52 | gulp.task('tag', ['commit'], function(cb){ 53 | console.log('Tagging Rebound as version ' + pjson.version); 54 | git.tag(''+pjson.version, "Rebound version v"+pjson.version, cb); 55 | }); 56 | 57 | gulp.task('push', ['tag'], function(cb){ 58 | console.log('Pushing Rebound v' + pjson.version); 59 | git.push('origin', 'master', {args: '--tags'}, cb); 60 | }); 61 | 62 | gulp.task('release', ['push'], function(cb){ 63 | git.status(); 64 | console.log('Rebound v'+pjson.version+' successfully released!'); 65 | return del(['tmp'], cb); 66 | }); -------------------------------------------------------------------------------- /packages/runtime.js: -------------------------------------------------------------------------------- 1 | // Rebound.js v%VER% 2 | 3 | // (c) 2015 Adam Miller 4 | // Rebound may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://reboundjs.com 7 | 8 | // Rebound Runtime 9 | // ---------------- 10 | 11 | // Import Backbone 12 | import Backbone from "backbone"; 13 | 14 | // Load our **Utils**, helper environment, **Rebound Data**, 15 | // **Rebound Components** and the **Rebound Router** 16 | import utils from "rebound-utils/rebound-utils"; 17 | import { Model, Collection, ComputedProperty } from "rebound-data/rebound-data"; 18 | import { services, Router } from "rebound-router/rebound-router"; 19 | import { registerHelper, registerPartial } from "rebound-htmlbars/rebound-htmlbars"; 20 | import { ComponentFactory, registerComponent } from "rebound-component/factory"; 21 | 22 | // Because of our bundle and how it plays with Backbone's UMD header, we need to 23 | // be a little more explicit with out DOM library search. 24 | Backbone.$ = window.$; 25 | 26 | // If Backbone doesn't have an ajax method from an external DOM library, use ours 27 | Backbone.ajax = Backbone.$ && Backbone.$.ajax && Backbone.ajax || utils.ajax; 28 | 29 | // Fetch Rebound's Config Object from Rebound's `script` tag 30 | var Config = document.getElementById('Rebound'); 31 | Config = (Config) ? JSON.parse(Config.innerHTML) : false; 32 | 33 | 34 | var Rebound = window.Rebound = { 35 | version: '%VER%', 36 | testing: (window.Rebound && window.Rebound.testing) || (Config && Config.testing) || false, 37 | 38 | registerHelper: registerHelper, 39 | registerPartial: registerPartial, 40 | registerComponent: registerComponent, 41 | 42 | Component: ComponentFactory, 43 | Model: Model, 44 | Collection: Collection, 45 | ComputedProperty: ComputedProperty, 46 | 47 | history: Backbone.history, 48 | services: services, 49 | start: function start(options){ 50 | var R = this; 51 | return new Promise(function(resolve, reject){ 52 | var run = function(){ 53 | if(!document.body){ return setTimeout(run.bind(R), 1); } 54 | delete R.router; 55 | R.router = new Router(options, resolve); 56 | }; 57 | run(); 58 | }); 59 | }, 60 | stop: function stop(){ 61 | if(!this.router) return console.error('No running Rebound router found!'); 62 | this.router.stop(); 63 | } 64 | }; 65 | 66 | // Start the router if a config object is preset 67 | if(Config){ Rebound.start(Config); } 68 | 69 | export default Rebound; 70 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "qunit", 3 | "test_page": "test/index.html", 4 | "cwd": "", 5 | "port": 8080, 6 | "launchers": { 7 | "SL_Chrome_Current": { 8 | "command": "saucie --browserNameSL='chrome' --versionSL='47.0' --platformSL='OS X 10.11' --ct=0", 9 | "protocol": "tap" 10 | }, 11 | "SL_Chrome_Last": { 12 | "command": "saucie --browserNameSL='chrome' --versionSL='46.0' --platformSL='OS X 10.11' --ct=0", 13 | "protocol": "tap" 14 | }, 15 | 16 | "SL_Firefox_Current": { 17 | "command": "saucie --browserNameSL='firefox' --versionSL='43.0' --platformSL='Linux' --ct=0", 18 | "protocol": "tap" 19 | }, 20 | "SL_Firefox_Last": { 21 | "command": "saucie --browserNameSL='firefox' --versionSL='42.0' --platformSL='Linux' --ct=0", 22 | "protocol": "tap" 23 | }, 24 | 25 | "SL_Safari_9": { 26 | "command": "saucie --browserNameSL='safari' --versionSL='9.0' --platformSL='OS X 10.11' --ct=0", 27 | "protocol": "tap" 28 | }, 29 | "SL_Safari_8": { 30 | "command": "saucie --browserNameSL='safari' --versionSL='8.0' --platformSL='OS X 10.10' --ct=0", 31 | "protocol": "tap" 32 | }, 33 | "SL_Safari_7": { 34 | "command": "saucie --browserNameSL='safari' --versionSL='7.0' --platformSL='OS X 10.9' --ct=0", 35 | "protocol": "tap" 36 | }, 37 | 38 | "SL_IE_11": { 39 | "command": "saucie --browserNameSL='internet explorer' --versionSL='11.0' --platformSL='Windows 10' --ct=0", 40 | "protocol": "tap" 41 | }, 42 | "SL_IE_10": { 43 | "command": "saucie --browserNameSL='internet explorer' --versionSL='10.0' --platformSL='Windows 8' --ct=0", 44 | "protocol": "tap" 45 | }, 46 | "SL_IE_9": { 47 | "command": "saucie --browserNameSL='internet explorer' --versionSL='9.0' --platformSL='Windows 7' --ct=0", 48 | "protocol": "tap" 49 | }, 50 | "SL_EDGE_20": { 51 | "command": "saucie --browserNameSL='microsoftedge' --versionSL='20.10240' --platformSL='Windows 10' --ct=0", 52 | "protocol": "tap" 53 | } 54 | }, 55 | "watch_files": [ 56 | "test/rebound.tests.js" 57 | ], 58 | "launch_in_dev": [ 59 | "PhantomJS", 60 | "Chrome" 61 | ], 62 | "launch_in_ci": [ 63 | "SL_Chrome_Current", 64 | "SL_Chrome_Last", 65 | "SL_Firefox_Current", 66 | "SL_Firefox_Last", 67 | "SL_Safari_9", 68 | "SL_Safari_8", 69 | "SL_Safari_7", 70 | "SL_IE_11", 71 | "SL_IE_10", 72 | "SL_IE_9", 73 | "SL_EDGE_20" 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /packages/rebound-data/test/rebound_collection_test.js: -------------------------------------------------------------------------------- 1 | import reboundData from 'rebound-data/rebound-data'; 2 | 3 | var Model = reboundData.Model, 4 | Collection = reboundData.Collection; 5 | 6 | QUnit.test('Reboudnd Data - Collection', function() { 7 | var model, collection; 8 | 9 | 10 | collection = new Collection(); 11 | collection.set({a:true}); 12 | equal(collection.models[0].isModel, true, 'Collection.set promotes vanilla objects to Models'); 13 | 14 | 15 | 16 | collection = new Collection(); 17 | collection.set({obj2:{a:true}}); 18 | equal(collection.models[0].attributes.obj2.isModel, true, 'Collection.set promotes nested vanilla objects to Models'); 19 | 20 | 21 | 22 | collection = new Collection(); 23 | collection.set({obj2:[{a:1}]}); 24 | equal(collection.models[0].attributes.obj2.isCollection, true, 'Collection.set promotes nested vanilla arrays to Collections'); 25 | 26 | 27 | collection = new Collection(); 28 | 29 | collection.set({'test2': [{'test3': 'foo'}]}); 30 | equal(collection.models[0].attributes.test2.models[0].__path(), '[0].test2[0]', 'Nested Models inherit path of parents'); 31 | equal(collection.models[0].attributes.test2.__path(), '[0].test2', 'Nested Collections inherit path of parents'); 32 | 33 | deepEqual(collection.toJSON(), [{'test2': [{'test3': 'foo'}]}], 'Collection\'s toJSON method is recursive'); 34 | collection.at(0).get('test2').at(0).set('test3', collection); 35 | deepEqual(collection.toJSON(), [{'test2': [{'test3': [collection.at(0).cid]}]}], 'Collection\'s toJSON handles cyclic dependancies'); 36 | 37 | equal(collection.at(0).__parent__.cid, collection.cid, 'Model\'s ancestry is set when child of a Collection'); 38 | 39 | collection.on('change', function(model, options){ 40 | deepEqual(model.changedAttributes(), {test2: 'foo'}, 'Events are propagated up to parent'); 41 | }); 42 | 43 | collection.at(0).set('test2', 'foo'); 44 | collection.off(); 45 | 46 | collection.set('[0].test2', 'bar'); 47 | deepEqual(collection.toJSON(), [{test2: 'bar'}], 'Collection.set can accept a path to call the .set at'); 48 | 49 | // Custom Model Constructors 50 | var CustomModel = Model.extend({ 51 | toJSON: function(){ 52 | return 'works'; 53 | }, 54 | idAttribute: 'foo.bar' 55 | }); 56 | 57 | model = new CustomModel({foo: {bar: 123}}); 58 | collection = new Collection(); 59 | collection.add({id: 1}); 60 | collection.add(model); 61 | 62 | 63 | deepEqual(collection.toJSON(), [{id: 1}, 'works'], 'Customized models added to a collection retain their custom attributes when added to the collection'); 64 | equal(model.cid, collection._byId[123].cid, 'Collections defer to the custom Model\'s id attribute when getting Model ids.'); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/test/rebound_helpers_register_test.js: -------------------------------------------------------------------------------- 1 | import compiler from 'rebound-compiler/compile'; 2 | import tokenizer from 'simple-html-tokenizer'; 3 | import helpers, { hasHelper, lookupHelper, registerHelper, registerPartial } from "rebound-htmlbars/helpers"; 4 | import Model from 'rebound-data/model'; 5 | 6 | function equalTokens(fragment, html, message) { 7 | var div = document.createElement("div"); 8 | 9 | div.appendChild(fragment.cloneNode(true)); 10 | 11 | var fragTokens = tokenizer.tokenize(div.innerHTML); 12 | var htmlTokens = tokenizer.tokenize(html); 13 | 14 | function normalizeTokens(token) { 15 | if (token.type === 'StartTag') { 16 | token.attributes = token.attributes.sort(function(a, b) { 17 | if (a.name > b.name) { 18 | return 1; 19 | } 20 | if (a.name < b.name) { 21 | return -1; 22 | } 23 | return 0; 24 | }); 25 | } 26 | } 27 | 28 | fragTokens.forEach(normalizeTokens); 29 | htmlTokens.forEach(normalizeTokens); 30 | 31 | deepEqual(fragTokens, htmlTokens, message); 32 | } 33 | 34 | 35 | /************************************************************ 36 | 37 | Register Helper 38 | 39 | *************************************************************/ 40 | 41 | QUnit.test('Rebound Helpers - Register', function(assert) { 42 | 43 | assert.expect(5); 44 | 45 | var func = function() { 46 | return 'test'; 47 | }; 48 | registerHelper('test', func); 49 | var regFunc = lookupHelper('test'); 50 | equal(func, regFunc, 'helpers.register adds a helper to the global scope which can be fetched by Helpers.lookupHelper'); 51 | 52 | 53 | var template, dom = document.createDocumentFragment(); 54 | 55 | template = compiler.compile('
{{doesnotexist foo bar}}
'); 56 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: false})); 57 | equalTokens(dom, '
', 'Using a helper that does not exist failes silently.'); 58 | 59 | template = compiler.compile('
{{test foo bar}}
'); 60 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: false})); 61 | equalTokens(dom, '
test
', 'Using a helper that does exist outputs the return value.'); 62 | 63 | 64 | template = compiler.compile('
{{if bool (doesnotexist foo)}}
'); 65 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: true})); 66 | equalTokens(dom, '
', 'Using a helper that does not exist in a subexpression fails silently.'); 67 | 68 | template = compiler.compile('
{{if bool (test)}}
'); 69 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: true})); 70 | equalTokens(dom, '
test
', 'Using a helper that does exist in a subexpression outputs the return value.'); 71 | 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /packages/rebound-compiler/test/rebound_precompiler_test.js: -------------------------------------------------------------------------------- 1 | import precompile from 'rebound-compiler/precompile'; 2 | 3 | function equalTokens(fragment, html) { 4 | var div = document.createElement("div"); 5 | 6 | div.appendChild(fragment.cloneNode(true)); 7 | 8 | var fragTokens = tokenizer.tokenize(div.innerHTML); 9 | var htmlTokens = tokenizer.tokenize(html); 10 | 11 | function normalizeTokens(token) { 12 | if (token.type === 'StartTag') { 13 | token.attributes = token.attributes.sort(function(a,b){ 14 | if (a.name > b.name) { 15 | return 1; 16 | } 17 | if (a.name < b.name) { 18 | return -1; 19 | } 20 | return 0; 21 | }); 22 | } 23 | } 24 | 25 | fragTokens.forEach(normalizeTokens); 26 | htmlTokens.forEach(normalizeTokens); 27 | 28 | deepEqual(fragTokens, htmlTokens); 29 | } 30 | 31 | 32 | QUnit.test('Rebound Precompiler', function( assert ) { 33 | var out, exp; 34 | 35 | assert.expect(2); 36 | 37 | out = precompile('
', {name: 'test/filepath'}).src.replace(/(\r\n|\n|\r)/gm," ").replace(/\s+/g," "); 38 | exp = '(function(R){ R.router._loadDeps([ ]); R.registerPartial("test/filepath", (function() { return { meta: {}, isEmpty: false, arity: 0, cachedFragment: null, hasRendered: false, buildFragment: function buildFragment(dom) { var el0 = dom.createDocumentFragment(); var el1 = dom.createElement("div"); dom.appendChild(el0, el1); return el0; }, buildRenderNodes: function buildRenderNodes() { return []; }, statements: [ ], locals: [], templates: [] }; }())); })(window.Rebound);'; 39 | 40 | assert.equal(out, exp, 'Pre-compiler can handle partials'); 41 | 42 | 43 | 44 | out = precompile( 45 | '\n' + 46 | ' \n' + 52 | ' \n' + 55 | '\n').src.replace(/(\r\n|\n|\r)/gm," ").replace(/\s+/g," "); 56 | exp = '(function(R){ R.router._loadDeps([ ]); document.currentScript.setAttribute("data-name", "test-element"); return R.registerComponent(\"test-element\", { prototype: (function(){ alert(0) })(), template: (function() { return { meta: {}, isEmpty: false, arity: 0, cachedFragment: null, hasRendered: false, buildFragment: function buildFragment(dom) { var el0 = dom.createDocumentFragment(); var el1 = dom.createTextNode(\"\\n \"); dom.appendChild(el0, el1); var el1 = dom.createElement(\"style\"); var el2 = dom.createTextNode(\"\\n .test{}\\n \"); dom.appendChild(el1, el2); dom.appendChild(el0, el1); var el1 = dom.createTextNode(\"\\n Test\\n \"); dom.appendChild(el0, el1); return el0; }, buildRenderNodes: function buildRenderNodes() { return []; }, statements: [ ], locals: [], templates: [] }; }()), stylesheet: \" .test{} \" }); })(window.Rebound);'; 57 | assert.equal(out, exp, 'Pre-compiler can handle components'); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/test/rebound_helpers_on_test.js: -------------------------------------------------------------------------------- 1 | import compiler from 'rebound-compiler/compile'; 2 | import tokenizer from 'simple-html-tokenizer'; 3 | import helpers from 'rebound-htmlbars/helpers'; 4 | import Model from 'rebound-data/model'; 5 | import hooks from 'rebound-htmlbars/hooks'; 6 | 7 | function equalTokens(fragment, html, message) { 8 | var div = document.createElement("div"); 9 | 10 | div.appendChild(fragment.cloneNode(true)); 11 | 12 | var fragTokens = tokenizer.tokenize(div.innerHTML); 13 | var htmlTokens = tokenizer.tokenize(html); 14 | 15 | function normalizeTokens(token) { 16 | if (token.type === 'StartTag') { 17 | token.attributes = token.attributes.sort(function(a, b) { 18 | if (a.name > b.name) { 19 | return 1; 20 | } 21 | if (a.name < b.name) { 22 | return -1; 23 | } 24 | return 0; 25 | }); 26 | } 27 | } 28 | 29 | fragTokens.forEach(normalizeTokens); 30 | htmlTokens.forEach(normalizeTokens); 31 | 32 | deepEqual(fragTokens, htmlTokens, message); 33 | } 34 | 35 | QUnit.test('Rebound Helpers - On', function(assert) { 36 | 37 | assert.expect(5); 38 | 39 | var data = { 40 | simpleCallback: function(){ 41 | window.delegateCallback = true; 42 | assert.equal(1, 1, 'Events are triggered on the element'); 43 | }, 44 | callbackWithData: function(event){ 45 | window.delegateCallback = true; 46 | assert.equal(event.data.foo, 'bar', 'Events are past hash values as event.data'); 47 | }, 48 | delegateCallback: function(event){ 49 | equal(window.delegateCallback, undefined, 'Events with a delegate are called before those without'); 50 | window.delegateCallback = true; 51 | assert.equal(event.target.tagName, 'LI', 'Events are triggered via delegates'); 52 | }, 53 | directCallback: function(event){ 54 | assert.equal(window.delegateCallback, undefined, 'Events bound directly to children elements are called before delegates higher in the dom tree'); 55 | } 56 | }; 57 | var options = { 58 | helpers: { 59 | _callOnComponent: function(name, event){ 60 | return data[name].call(data, event); 61 | } 62 | } 63 | }; 64 | 65 | var template = compiler.compile(``, {name: 'test/partial'}); 75 | 76 | var dom = document.createDocumentFragment(); 77 | template.render(dom, data, options); 78 | // In PhantomJS, document fragments don't have a firstElementChild property 79 | dom = dom.firstChild; 80 | document.body.appendChild(dom); 81 | var event = document.createEvent('Event'); 82 | event.initEvent('click', true, true); 83 | dom.firstElementChild.firstElementChild.dispatchEvent(event); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /tasks/docco.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var concat = require('gulp-concat'); 3 | var docco = require("gulp-docco"); 4 | 5 | /******************************************************************************* 6 | 7 | Docco Task: 8 | 9 | docco is run on prepublish to automatically generate annotated source code in 10 | /dist. 11 | 12 | *******************************************************************************/ 13 | 14 | gulp.task('docco', [], function() { 15 | return gulp.src([ 16 | 'packages/runtime.js', 17 | 18 | // Rebound Data 19 | 'packages/rebound-data/lib/rebound-data.js', 20 | 'packages/rebound-data/lib/model.js', 21 | 'packages/rebound-data/lib/collection.js', 22 | 'packages/rebound-data/lib/computed-property.js', 23 | 24 | // Rebound Component 25 | 'packages/rebound-component/lib/component.js', 26 | 'packages/rebound-component/lib/factory.js', 27 | 28 | // Rebound View 29 | 'packages/rebound-htmlbars/lib/rebound-htmlbars.js', 30 | 'packages/rebound-htmlbars/lib/lazy-value.js', 31 | 'packages/rebound-htmlbars/lib/hooks.js', 32 | 'packages/rebound-htmlbars/lib/hooks/createFreshEnv.js', 33 | 'packages/rebound-htmlbars/lib/hooks/createChildEnv.js', 34 | 35 | 'packages/rebound-htmlbars/lib/hooks/createFreshScope.js', 36 | 'packages/rebound-htmlbars/lib/hooks/createChildScope.js', 37 | 'packages/rebound-htmlbars/lib/hooks/bindScope.js', 38 | 39 | 'packages/rebound-htmlbars/lib/hooks/linkRenderNode.js', 40 | 'packages/rebound-htmlbars/lib/hooks/willCleanupTree.js', 41 | 'packages/rebound-htmlbars/lib/hooks/cleanupRenderNode.js', 42 | 'packages/rebound-htmlbars/lib/hooks/destroyRenderNode.js', 43 | 'packages/rebound-htmlbars/lib/hooks/didCleanupTree.js', 44 | 45 | 'packages/rebound-htmlbars/lib/hooks/get.js', 46 | 'packages/rebound-htmlbars/lib/hooks/invokeHelper.js', 47 | 'packages/rebound-htmlbars/lib/hooks/getValue.js', 48 | 'packages/rebound-htmlbars/lib/hooks/subexpr.js', 49 | 'packages/rebound-htmlbars/lib/hooks/concat.js', 50 | 51 | 'packages/rebound-htmlbars/lib/hooks/content.js', 52 | 'packages/rebound-htmlbars/lib/hooks/attribute.js', 53 | 'packages/rebound-htmlbars/lib/hooks/partial.js', 54 | 'packages/rebound-htmlbars/lib/hooks/component.js', 55 | 56 | 'packages/rebound-htmlbars/lib/helpers.js', 57 | 58 | 'packages/rebound-htmlbars/lib/compile.js', 59 | 'packages/rebound-htmlbars/lib/render.js', 60 | 61 | 62 | 63 | // Rebound Router 64 | 'packages/rebound-router/lib/rebound-router.js', 65 | 'packages/rebound-router/lib/loader.js', 66 | 67 | // Rebound Utils 68 | 'packages/rebound-component/lib/rebound-utils.js', 69 | 'packages/rebound-component/lib/ajax.js', 70 | 'packages/rebound-component/lib/events.js', 71 | 'packages/rebound-component/lib/urls.js', 72 | 73 | 'packages/property-compiler/lib/property-compiler.js' 74 | ]) 75 | .pipe(concat('rebound.js')) 76 | .pipe(docco()) 77 | .pipe(gulp.dest('dist/docs')); 78 | }); 79 | -------------------------------------------------------------------------------- /packages/rebound-utils/lib/ajax.js: -------------------------------------------------------------------------------- 1 | // Rebound AJAX 2 | // ---------------- 3 | 4 | // Rebound includes its own ajax method so that it not dependant on a largeer library 5 | // like jQuery. Here we expose the `ajax` method which mirrors jQuery's ajax API. 6 | // This methods is added to Rebound's internal utility library and used throughout the framework. 7 | // Inspiration: http://krasimirtsonev.com/blog/article/Cross-browser-handling-of-Ajax-requests-in-absurdjs 8 | 9 | import { query } from "rebound-utils/urls"; 10 | 11 | export default function ajax(ops) { 12 | if(typeof ops == 'string') ops = { url: ops }; 13 | ops.url = ops.url || ''; 14 | ops.json = ops.json || true; 15 | ops.method = ops.method || 'get'; 16 | ops.data = ops.data || {}; 17 | var api = { 18 | host: {}, 19 | process: function(ops) { 20 | var self = this; 21 | this.xhr = null; 22 | if(window.ActiveXObject) { this.xhr = new ActiveXObject('Microsoft.XMLHTTP'); } 23 | else if(window.XMLHttpRequest) { this.xhr = new XMLHttpRequest(); } 24 | if(this.xhr) { 25 | this.xhr.onreadystatechange = function() { 26 | if(self.xhr.readyState == 4 && self.xhr.status == 200) { 27 | var result = self.xhr.responseText; 28 | if(ops.json === true && typeof JSON != 'undefined') { 29 | result = JSON.parse(result); 30 | } 31 | self.doneCallback && self.doneCallback.apply(self.host, [result, self.xhr]); 32 | ops.success && ops.success.apply(self.host, [result, self.xhr]); 33 | } else if(self.xhr.readyState == 4) { 34 | self.failCallback && self.failCallback.apply(self.host, [self.xhr]); 35 | ops.error && ops.error.apply(self.host, [self.xhr]); 36 | } 37 | self.alwaysCallback && self.alwaysCallback.apply(self.host, [self.xhr]); 38 | ops.complete && ops.complete.apply(self.host, [self.xhr]); 39 | }; 40 | } 41 | if(ops.method == 'get') { 42 | this.xhr.open("GET", ops.url + query.stringify(ops.data), true); 43 | this.setHeaders({ 44 | 'X-Requested-With': 'XMLHttpRequest' 45 | }); 46 | } else { 47 | this.xhr.open(ops.method, ops.url, true); 48 | this.setHeaders({ 49 | 'X-Requested-With': 'XMLHttpRequest', 50 | 'Content-type': 'application/x-www-form-urlencoded' 51 | }); 52 | } 53 | if(ops.headers && typeof ops.headers == 'object') { 54 | this.setHeaders(ops.headers); 55 | } 56 | setTimeout(function() { 57 | ops.method == 'get' ? self.xhr.send() : self.xhr.send(query.stringify(ops.data)); 58 | }, 20); 59 | return this.xhr; 60 | }, 61 | done: function(callback) { 62 | this.doneCallback = callback; 63 | return this; 64 | }, 65 | fail: function(callback) { 66 | this.failCallback = callback; 67 | return this; 68 | }, 69 | always: function(callback) { 70 | this.alwaysCallback = callback; 71 | return this; 72 | }, 73 | setHeaders: function(headers) { 74 | for(var name in headers) { 75 | this.xhr && this.xhr.setRequestHeader(name, headers[name]); 76 | } 77 | } 78 | }; 79 | return api.process(ops); 80 | } 81 | -------------------------------------------------------------------------------- /packages/property-compiler/lib/property-compiler.js: -------------------------------------------------------------------------------- 1 | // Property Compiler 2 | // ---------------- 3 | 4 | import { Parser, tokTypes } from "acorn"; 5 | 6 | function tokenizer(input, options) { 7 | return new Parser(options, input); 8 | } 9 | 10 | const TERMINATORS = [';',',','==','>','<','>=','<=','>==','<==','!=','!==', '===', '&&', '||', '+', '-', '/', '*', '{', '}']; 11 | 12 | function reduceMemos(memo, paths){ 13 | var newMemo = []; 14 | paths = (!_.isArray(paths)) ? [paths] : paths; 15 | _.each(paths, function(path){ 16 | _.each(memo, function(mem){ 17 | newMemo.push(_.compact([mem, path]).join('.').replace('.[', '[')); 18 | }); 19 | }); 20 | return newMemo; 21 | } 22 | 23 | // TODO: Make this farrrrrr more robust...very minimal right now 24 | 25 | function compile(prop, name){ 26 | var output = {}; 27 | 28 | if(prop.__params) return prop.__params; 29 | 30 | var str = prop.toString(), //.replace(/(?:\/\*(?:[\s\S]*?)\*\/)|(?:([\s;])+\/\/(?:.*)$)/gm, '$1'), // String representation of function sans comments 31 | token = tokenizer(str, { 32 | ecmaVersion: 6, 33 | sourceType: 'script' 34 | }), 35 | finishedPaths = [], 36 | listening = 0, 37 | paths = [], 38 | attrs = [], 39 | workingpath = []; 40 | 41 | do { 42 | 43 | // console.log(token.type.label, token.value); 44 | token.nextToken(); 45 | 46 | if(token.value === 'this'){ 47 | listening++; 48 | workingpath = []; 49 | } 50 | 51 | // TODO: handle gets on collections 52 | if(token.value === 'get'){ 53 | token.nextToken(); 54 | while(_.isUndefined(token.value)){ 55 | token.nextToken(); 56 | } 57 | // Replace any access to a collection with the generic @each placeholder and push dependancy 58 | workingpath.push(token.value.replace(/\[.+\]/g, ".@each").replace(/^\./, '')); 59 | } 60 | 61 | if(token.value === 'pluck'){ 62 | token.nextToken(); 63 | while(_.isUndefined(token.value)){ 64 | token.nextToken(); 65 | } 66 | 67 | workingpath.push('@each.' + token.value); 68 | } 69 | 70 | if(token.value === 'slice' || token.value === 'clone' || token.value === 'filter'){ 71 | token.nextToken(); 72 | if(token.type.label === '(') workingpath.push('@each'); 73 | } 74 | 75 | if(token.value === 'at'){ 76 | token.nextToken(); 77 | while(_.isUndefined(token.value)){ 78 | token.nextToken(); 79 | } 80 | workingpath.push('@each'); 81 | } 82 | 83 | if(token.value === 'where' || token.value === 'findWhere'){ 84 | workingpath.push('@each'); 85 | token.nextToken(); 86 | attrs = []; 87 | var itr = 0; 88 | while(token.type.label !== ')'){ 89 | if(token.value){ 90 | if(itr%2 === 0){ 91 | attrs.push(token.value); 92 | } 93 | itr++; 94 | } 95 | token.nextToken(); 96 | } 97 | workingpath.push(attrs); 98 | } 99 | 100 | if(listening && (_.indexOf(TERMINATORS, token.type.label) > -1 || _.indexOf(TERMINATORS, token.value) > -1)){ 101 | workingpath = _.reduce(workingpath, reduceMemos, ['']); 102 | finishedPaths = _.compact(_.union(finishedPaths, workingpath)); 103 | workingpath = []; 104 | listening--; 105 | } 106 | 107 | } while (token.start !== token.end && token.type !== tokTypes.eof); 108 | 109 | // Save our finished paths directly on the function 110 | prop.__params = finishedPaths; 111 | 112 | // Return the dependancies list 113 | return finishedPaths; 114 | 115 | } 116 | 117 | export default { compile: compile }; 118 | -------------------------------------------------------------------------------- /packages/rebound-compiler/lib/parser.js: -------------------------------------------------------------------------------- 1 | // Rebound Template Parser 2 | // ----------------------- 3 | 4 | // Remove the contents of the component's `script` tag. 5 | function getScript(str) { 6 | var start = str.lastIndexOf(''); 7 | str = str.slice(((start > -1) ? start : 0), str.length); 8 | start = str.indexOf(''); 10 | 11 | if(start > -1 && end > -1) 12 | return '(function(){' + str.substring((start + 8), end) + '})()'; 13 | return '{}'; 14 | } 15 | 16 | // Remove the contents of the component's `style` tag. 17 | function getStyle(str) { 18 | var start = str.indexOf(""); 20 | return start > -1 && end > -1 ? str.substr(start + 7, end - (start + 7)).replace(/"/g, "\\\"") : ""; 21 | } 22 | 23 | function stripLinkTags(str){ 24 | // Remove link tags from template, these are fetched in getDependancies 25 | return str.replace(/]*>/gi, ''); 26 | } 27 | 28 | // Remove the contents of the component's `template` tag. 29 | function getTemplate(str) { 30 | var start = str.indexOf("'); 32 | 33 | // Get only the content between the template tags, or set to an empty string. 34 | str = (start > -1 && end > -1) ? str.substring((start + 10), end) : ''; 35 | 36 | return stripLinkTags(str); 37 | } 38 | 39 | // Get the component's name from its `name` attribute. 40 | function getName(str) { 41 | return str.replace(/[^]*?]*name=(["'])?([^'">\s]+)\1[^<>]*>[^]*/ig, "$2").trim(); 42 | } 43 | 44 | // Minify the string passed in by replacing all whitespace. 45 | function minify(str) { 46 | return str.replace(/\s+/g, " ").replace(/\n|(>) (<)/g, "$1$2"); 47 | } 48 | 49 | // Strip javascript comments 50 | function removeComments(str) { 51 | return str.replace(/(?:\/\*(?:[\s\S]*?)\*\/)|(?:([\s])+\/\/(?:.*)$)/gm, "$1"); 52 | } 53 | 54 | // TODO: This is messy, clean it up! 55 | function getDependancies(template){ 56 | var imports = [], 57 | partials = [], 58 | deps = [], 59 | match, 60 | importsre = /]*>/gi, 61 | partialsre = /\{\{>\s*?(['"])?([^'"}\s]*)\1\s*?\}\}/gi, 62 | helpersre = /\{\{partial\s*?(['"])([^'"}\s]*)\1\s*?\}\}/gi, 63 | start = template.indexOf("'); 65 | 66 | if(start > -1 && end > -1) { template = template.substring((start + 10), end); } 67 | 68 | // Assemble our imports dependancies 69 | (template.match(importsre) || []).forEach(function(importString, index){ 70 | deps.push(importString.replace(importsre, '$2')); 71 | }); 72 | 73 | // Assemble our partial dependancies 74 | (template.match(partialsre) || []).forEach(function(partial, index){ 75 | deps.push(partial.replace(partialsre, '$2')); 76 | }); 77 | 78 | // Assemble our partial dependancies 79 | (template.match(helpersre) || []).forEach(function(partial, index){ 80 | deps.push(partial.replace(helpersre, '$2')); 81 | }); 82 | 83 | return deps; 84 | } 85 | 86 | function parse(str, options={}){ 87 | // If the element tag is present 88 | if(str.indexOf(' -1 && str.indexOf('') > -1){ 89 | return { 90 | isPartial: false, 91 | name: getName(str), 92 | stylesheet: getStyle(str), 93 | template: getTemplate(str), 94 | script: getScript(str), 95 | deps: getDependancies(str) 96 | }; 97 | } 98 | 99 | return { 100 | isPartial: true, 101 | name: options.name, 102 | template: stripLinkTags(str), 103 | deps: getDependancies(str) 104 | }; 105 | 106 | } 107 | 108 | export default parse; 109 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks.js: -------------------------------------------------------------------------------- 1 | // Rebound Hooks 2 | // ---------------- 3 | // Here we augment HTMLBars' default hooks to make use of Rebound's evented data 4 | // objects for automatic databinding. 5 | 6 | import $ from "rebound-utils/rebound-utils"; 7 | import hooks from "htmlbars-runtime/hooks"; 8 | import { default as _render } from "htmlbars-runtime/render"; 9 | 10 | // __Environment Hooks__ create and modify the template environment objects 11 | import createFreshEnv from "rebound-htmlbars/hooks/createFreshEnv"; 12 | import createChildEnv from "rebound-htmlbars/hooks/createChildEnv"; 13 | hooks.createFreshEnv = createFreshEnv; 14 | hooks.createChildEnv = createChildEnv; 15 | 16 | 17 | // __Scope Hooks__ create, access and modify the template scope and data objects 18 | import createFreshScope from "rebound-htmlbars/hooks/createFreshScope"; 19 | import createChildScope from "rebound-htmlbars/hooks/createChildScope"; 20 | import bindScope from "rebound-htmlbars/hooks/bindScope"; 21 | hooks.createFreshScope = createFreshScope; 22 | hooks.createChildScope = createChildScope; 23 | hooks.bindScope = bindScope; 24 | 25 | 26 | // __Lifecycle Hooks__ construct, deconstruct and clean up render nodes over their lifecycles 27 | import linkRenderNode from "rebound-htmlbars/hooks/linkRenderNode"; 28 | import cleanupRenderNode from "rebound-htmlbars/hooks/cleanupRenderNode"; 29 | import destroyRenderNode from "rebound-htmlbars/hooks/destroyRenderNode"; 30 | import willCleanupTree from "rebound-htmlbars/hooks/willCleanupTree"; 31 | import didCleanupTree from "rebound-htmlbars/hooks/didCleanupTree"; 32 | hooks.linkRenderNode = linkRenderNode; 33 | hooks.willCleanupTree = willCleanupTree; 34 | hooks.cleanupRenderNode = cleanupRenderNode; 35 | hooks.destroyRenderNode = cleanupRenderNode; 36 | hooks.didCleanupTree = didCleanupTree; 37 | 38 | 39 | // __Streaming Hooks__ create streams via LazyValues for data values, helpers, subexpressions and concat groups 40 | import get from "rebound-htmlbars/hooks/get"; 41 | import getValue from "rebound-htmlbars/hooks/getValue"; 42 | import invokeHelper from "rebound-htmlbars/hooks/invokeHelper"; 43 | import subexpr from "rebound-htmlbars/hooks/subexpr"; 44 | import concat from "rebound-htmlbars/hooks/concat"; 45 | hooks.get = get; 46 | hooks.getValue = getValue; 47 | hooks.invokeHelper = invokeHelper; 48 | hooks.subexpr = subexpr; 49 | hooks.concat = concat; 50 | 51 | 52 | // __Render Hooks__ interact with the DOM to output content and bind to form elements for two way databinding 53 | import content from "rebound-htmlbars/hooks/content"; 54 | import attribute from "rebound-htmlbars/hooks/attribute"; 55 | import partial, { registerPartial } from "rebound-htmlbars/hooks/partial"; 56 | import component from "rebound-htmlbars/hooks/component"; 57 | hooks.content = content; 58 | hooks.attribute = attribute; 59 | hooks.partial = partial; 60 | hooks.registerPartial = registerPartial; 61 | hooks.component = component; 62 | 63 | 64 | // __Helper Hooks__ manage the environment's registered helpers 65 | import { hasHelper, lookupHelper, registerHelper } from "rebound-htmlbars/helpers"; 66 | hooks.hasHelper = hasHelper; 67 | hooks.lookupHelper = lookupHelper; 68 | hooks.registerHelper = registerHelper; 69 | 70 | 71 | // Bind local binds a local variable to the scope object and tracks the scope 72 | // level at which that local was added. See `createChildScope` for description 73 | // of scope levels 74 | hooks.bindLocal = function bindLocal(env, scope, name, value){ 75 | scope.localPresent[name] = scope.level; 76 | scope.locals[name] = value; 77 | }; 78 | 79 | 80 | // __buildRenderResult__ is a wrapper for the native HTMLBars render function. It 81 | // ensures every template is rendered with its own child environment, every environment 82 | // saves a referance to its unique render result for re-renders, and every render 83 | // result has a unique id. 84 | hooks.buildRenderResult = function buildRenderResult(template, env, scope, options){ 85 | var render = _render.default || _render; // Fix for stupid Babel imports 86 | env = hooks.createChildEnv(env); 87 | env.template = render(template, env, scope, options); 88 | env.template.uid = $.uniqueId('template'); 89 | return env.template; 90 | }; 91 | 92 | export default hooks; 93 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/hooks/attribute.js: -------------------------------------------------------------------------------- 1 | // ### Attribute Hook 2 | 3 | import $ from "rebound-utils/rebound-utils"; 4 | 5 | // All valid text based HTML input types 6 | const TEXT_INPUTS = { "null": 1, text: 1, email: 1, password: 1, 7 | search: 1, url: 1, tel: 1, hidden: 1, 8 | number: 1, color: 1, date: 1, datetime: 1, 9 | month: 1, range: 1, time: 1, week: 1, 10 | "datetime-local": 1 11 | }; 12 | 13 | // All valid boolean HTML input types 14 | const BOOLEAN_INPUTS = { checkbox: 1, radio: 1 }; 15 | 16 | // Returns true is value is numeric based on HTML5 number input field logic. 17 | // Trailing decimals are considered non-numeric (ex `12.`). 18 | function isNumeric(val){ 19 | return val && !isNaN(Number(val)) && (!_.isString(val) || ((_.isString(val) && val[val.length-1] !== '.'))); 20 | } 21 | 22 | // Attribute Hook 23 | export default function attribute(attrMorph, env, scope, name, value){ 24 | 25 | var val = value.isLazyValue ? value.value : value, 26 | el = attrMorph.element, 27 | tagName = el.tagName, 28 | type = el.getAttribute("type"), 29 | cursor = false; 30 | 31 | 32 | // If this is a text input element's value prop, wire up our databinding 33 | if( tagName === 'INPUT' && type === 'number' && name === 'value' ){ 34 | 35 | // If our input events have not been bound yet, bind them. Attempt to convert 36 | // to a proper number type before setting. 37 | if(!attrMorph.eventsBound){ 38 | $(el).on('change input propertychange', function(event){ 39 | var val = this.value; 40 | val = isNumeric(val) ? Number(val) : undefined; 41 | value.set(value.path, val); 42 | }); 43 | attrMorph.eventsBound = true; 44 | } 45 | 46 | // Set the value property of the input 47 | // Number Input elements may return `''` for non valid numbers. If both values 48 | // are falsy, then don't blow away what the user is typing. 49 | if(!el.value && !val){ return; } 50 | else{ el.value = isNumeric(val) ? Number(val) : ''; } 51 | 52 | } 53 | 54 | // If this is a text input element's value prop, wire up our databinding 55 | else if( tagName === 'INPUT' && TEXT_INPUTS[type] && name === 'value' ){ 56 | 57 | // If our input events have not been bound yet, bind them 58 | if(!attrMorph.eventsBound){ 59 | $(el).on('change input propertychange', function(event){ 60 | value.set(value.path, this.value); 61 | }); 62 | attrMorph.eventsBound = true; 63 | } 64 | 65 | // Set the value property of the input if it has changed 66 | if(el.value !== val){ 67 | 68 | // Only save the cursor position if this element is the currently focused one. 69 | // Some browsers are dumb about selectionStart on some input types (I'm looking at you [type='email']) 70 | // so wrap in try catch so it doesn't explode. Then, set the new value and 71 | // re-position the cursor. 72 | if(el === document.activeElement){ try{ cursor = el.selectionStart; } catch(e){ } } 73 | el.value = val ? String(val) : ''; 74 | (cursor !== false) && el.setSelectionRange(cursor, cursor); 75 | 76 | } 77 | } 78 | 79 | else if( tagName === 'INPUT' && BOOLEAN_INPUTS[type] && name === 'checked' ){ 80 | 81 | // If our input events have not been bound yet, bind them 82 | if(!attrMorph.eventsBound){ 83 | $(el).on('change propertychange', function(event){ 84 | value.set(value.path, ((this.checked) ? true : false)); 85 | }); 86 | attrMorph.eventsBound = true; 87 | } 88 | 89 | el.checked = (val) ? true : undefined; 90 | } 91 | 92 | // Special case for link elements with dynamic classes. 93 | // If the router has assigned it a truthy 'active' property, ensure that the extra class is present on re-render. 94 | else if( tagName === 'A' && name === 'class' && el.active ){ 95 | val = val ? String(val) + ' active' : 'active'; 96 | } 97 | 98 | // Set the attribute on our element for visual referance 99 | val ? el.setAttribute(name, String(val)) : el.removeAttribute(name); 100 | 101 | this.linkRenderNode(attrMorph, env, scope, '@attribute', [value], {}); 102 | 103 | } 104 | -------------------------------------------------------------------------------- /packages/rebound-router/lib/loader.js: -------------------------------------------------------------------------------- 1 | import $ from "rebound-utils/rebound-utils"; 2 | 3 | var MODULE_CACHE = {}; 4 | 5 | var loader = { 6 | 7 | // If this JS element is not on the page already, it hasn't been loaded before - 8 | // create the element and load the JS resource. 9 | // Else if the JS resource has been loaded before, resolve with the element 10 | loadJS(url, id){ 11 | 12 | // Always return a promise for a load request 13 | return new Promise(function(resolve, reject){ 14 | 15 | // If we have already tried to load this js module, resolve or reject appropreately 16 | if(MODULE_CACHE[url]){ 17 | if (_.isElement(MODULE_CACHE[url]) && MODULE_CACHE[url].hasAttribute('data-error')){ return reject(); } 18 | return resolve(MODULE_CACHE[url]); 19 | } 20 | 21 | // Construct the script element and save it in the `MODULE_CACHE` 22 | var e = document.createElement('script'); 23 | e.setAttribute('type', 'text/javascript'); 24 | e.setAttribute('src', url); 25 | e.setAttribute('id', (id || _.uniqueId('module')) ); 26 | MODULE_CACHE[url] = e; 27 | 28 | // All browsers support loading events on ` 197 | -------------------------------------------------------------------------------- /packages/rebound-component/lib/factory.js: -------------------------------------------------------------------------------- 1 | // Rebound Component Factory 2 | // ---------------- 3 | 4 | import { $, REBOUND_SYMBOL } from "rebound-utils/rebound-utils"; 5 | import Component from "rebound-component/component"; 6 | 7 | var REGISTRY = {}; 8 | const DUMMY_TEMPLATE = false; 9 | 10 | // Used to transport component specific data to the native element created callback 11 | // in leu of a good API for passing initialization data to document.createElement. 12 | // When registry.create is called, it stashes instance data on this object in a 13 | // shared scope. After createElement is finished, it cleans this transport object 14 | var ELEMENT_DATA; 15 | 16 | export function registerComponent(type, options={}) { 17 | // Ensure our options are set nicely and extract the prototype provided to us 18 | var proto = options.prototype || {}; 19 | delete options.prototype; 20 | options.type = type; 21 | options.isHydrated = true; 22 | 23 | // If the component exists in the registry, and is already hydrated, then this 24 | // is a conflicting component name – exit and log an error. 25 | if(REGISTRY[type] && REGISTRY[type].isHydrated){ 26 | return console.error('A component of type', type, 'already exists!'); 27 | } 28 | 29 | // If there is a non-hydrated component in the registry, hydrate it with the 30 | // newly provided prototype. 31 | if(REGISTRY[type]){ 32 | REGISTRY[type].hydrate(proto, options); 33 | } 34 | 35 | // Otherwise, create and save a new component subclass and register the element 36 | else { 37 | REGISTRY[type] = Component.extend(proto, options); 38 | } 39 | 40 | // Create our new element prototype object 41 | var element = Object.create(HTMLElement.prototype, {}); 42 | 43 | // On element creation, make a new instance of the component and attach it 44 | // to the element object as `data` 45 | element.createdCallback = function() { 46 | 47 | // If `this.data` already exists on this element, then it was present on the 48 | // page via a `new Component(component-name);` call before this component was 49 | // actually registered. Now, we need to finish hydrating this instance of the 50 | // component data object. 51 | if(this.data){ 52 | 53 | // Anything that is not already set on our component should be set to our 54 | // new default if it exists 55 | // TODO: If a default value perscribes a certain user-defined subclass 56 | // of Component or Model for a property already passed into a component, 57 | // the existing vanila Component or Model should be upgraded to that subclass 58 | var current = this.data.toJSON(); 59 | var defaults = this.data.defaults; 60 | for(var key in defaults){ 61 | if ((!current.hasOwnProperty(key) || _.isUndefined(current[key])) && defaults.hasOwnProperty(key)) { 62 | this.data.set(key, defaults[key]); 63 | } 64 | } 65 | this.data.render(); 66 | this.data.isHydrated = true; 67 | this.data.loadCallbacks.forEach( (cb)=>{ cb.call(this.data, this.data); } ); 68 | } 69 | 70 | // If we have element data, then we have come from a `new Component(component-name);` 71 | // call and may have been provided data to initialize with. Call the component 72 | // constructor with the provided properties. We don't need `new` here because 73 | // the instance we are building is provided for us, so we use `component.call` 74 | // to call the component constructor using that scope. 75 | else if(ELEMENT_DATA){ 76 | this.data = new REGISTRY[type](this, ELEMENT_DATA.data, ELEMENT_DATA.options); 77 | } 78 | 79 | // Otherwise, this is an upgraded instance of the element that was pre-existing 80 | // in the dom, or just created using `document.createElement`. Go ahead and 81 | // give it a new component object. 82 | else { this.data = new REGISTRY[type](this); } 83 | 84 | // Call user provided `attachedCallback` 85 | _.isFunction(proto.createdCallback) && proto.createdCallback.call(this.data); 86 | }; 87 | 88 | // Call user provided `attachedCallback` 89 | element.attachedCallback = function() { 90 | _.isFunction(proto.attachedCallback) && proto.attachedCallback.call(this.data); 91 | }; 92 | 93 | // Call user provided `detachedCallback` 94 | element.detachedCallback = function() { 95 | _.isFunction(proto.detachedCallback) && proto.detachedCallback.call(this.data); 96 | }; 97 | 98 | // Call user provided `attributeChangedCallback` 99 | element.attributeChangedCallback = function(attrName, oldVal, newVal) { 100 | if(!this.data){ return; } 101 | this.data._onAttributeChange(attrName, oldVal, newVal); 102 | _.isFunction(proto.attributeChangedCallback) && proto.attributeChangedCallback.call(this.data, attrName, oldVal, newVal); 103 | }; 104 | 105 | // Register our new element 106 | document.registerElement(type, { prototype: element }); 107 | 108 | // Return the new component constructor 109 | return REGISTRY[type]; 110 | } 111 | 112 | 113 | export var ComponentFactory = function ComponentFactory(type, data={}, options={}){ 114 | 115 | // If type is not a valid component name, error 116 | if(typeof type !== 'string'){ return console.error('Invalid component type provided to createComponent. Instead received:', type); } 117 | 118 | var el; 119 | 120 | // If this component is not in the registry, register a dehydrated component 121 | // as a placeholder. Once the actual component is loaded, all running instances 122 | // of this component type will be hydrated. 123 | if(!REGISTRY[type] || !REGISTRY[type].isHydrated){ 124 | el = document.createElement(type); 125 | options.isHydrated = false; 126 | REGISTRY[type] = REGISTRY[type] || Component.extend({}, { 127 | isHydrated: false, 128 | type: type, 129 | template: DUMMY_TEMPLATE 130 | }, options); 131 | el.data = new REGISTRY[type](el, data, options); 132 | } 133 | 134 | // If this component is in the registry, save the instance specific data to 135 | // deliver to the createElement call, and create the element. As part of the 136 | // `createdCallback` a new instance of 137 | else { 138 | ELEMENT_DATA = { data: data, options: options }; 139 | el = document.createElement(type); 140 | ELEMENT_DATA = void 0; 141 | } 142 | 143 | return el.data; 144 | 145 | }; 146 | 147 | ComponentFactory.registerComponent = registerComponent; 148 | 149 | export default ComponentFactory; 150 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/test/rebound_helpers_if_test.js: -------------------------------------------------------------------------------- 1 | import compiler from 'rebound-compiler/compile'; 2 | import tokenizer from 'simple-html-tokenizer'; 3 | import helpers from 'rebound-htmlbars/helpers'; 4 | import Model from 'rebound-data/model'; 5 | 6 | 7 | function equalTokens(fragment, html, message) { 8 | var div = document.createElement("div"); 9 | 10 | div.appendChild(fragment.cloneNode(true)); 11 | 12 | var fragTokens = tokenizer.tokenize(div.innerHTML); 13 | var htmlTokens = tokenizer.tokenize(html); 14 | 15 | function normalizeTokens(token) { 16 | if (token.type === 'StartTag') { 17 | token.attributes = token.attributes.sort(function(a, b) { 18 | if (a.name > b.name) { 19 | return 1; 20 | } 21 | if (a.name < b.name) { 22 | return -1; 23 | } 24 | return 0; 25 | }); 26 | } 27 | } 28 | 29 | fragTokens.forEach(normalizeTokens); 30 | htmlTokens.forEach(normalizeTokens); 31 | 32 | deepEqual(fragTokens, htmlTokens, message); 33 | } 34 | 35 | 36 | /************************************************************ 37 | 38 | If 39 | 40 | *************************************************************/ 41 | 42 | QUnit.test('Rebound Helpers - If', function() { 43 | 44 | var template, data, dom = document.createDocumentFragment(); 45 | 46 | template = compiler.compile('
{{#if bool}}{{foo}}{{/if}}
', {name: 'test/partial'}); 47 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: false})); 48 | equalTokens(dom, '
', 'Block If helper without else block - false'); 49 | 50 | 51 | template = compiler.compile('
{{#if bool}}{{foo}}{{else}}{{bar}}{{/if}}
', {name: 'test/partial'}); 52 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: false})); 53 | equalTokens(dom, '
foo
', 'Block If helper with else block - false'); 54 | 55 | 56 | 57 | template = compiler.compile('
{{#if bool}}{{foo}}{{/if}}
', {name: 'test/partial'}); 58 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: true})); 59 | equalTokens(dom, '
bar
', 'Block If helper without else block - true'); 60 | 61 | 62 | 63 | template = compiler.compile('
{{#if bool}}{{foo}}{{else}}{{bar}}{{/if}}
', {name: 'test/partial'}); 64 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: true})); 65 | equalTokens(dom, '
bar
', 'Block If helper with else block - true'); 66 | 67 | 68 | 69 | template = compiler.compile('
{{#if bool}}{{foo}}{{else}}{{bar}}{{/if}}
', {name: 'test/partial'}); 70 | data = new Model({foo:'bar', bar:'foo', bool: false}); 71 | template.render(dom, data); 72 | data.set('bool', true); 73 | equalTokens(dom, '
bar
', 'Block If helper is data bound'); 74 | data.set('bool', false); 75 | equalTokens(dom, '
foo
', 'Block If helper is data bound and returns to old value'); 76 | 77 | 78 | 79 | template = compiler.compile('
{{if bool foo}}
', {name: 'test/partial'}); 80 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: true})); 81 | equalTokens(dom, '
bar
', 'Inline If helper in content without else term - true'); 82 | 83 | 84 | 85 | template = compiler.compile('
{{if bool foo bar}}
', {name: 'test/partial'}); 86 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: true})); 87 | equalTokens(dom, '
bar
', 'Inline If helper in content with else term - true'); 88 | 89 | 90 | 91 | template = compiler.compile('
{{if bool foo}}
', {name: 'test/partial'}); 92 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: false})); 93 | equalTokens(dom, '
', 'Inline If helper in content without else term - false'); 94 | 95 | 96 | 97 | template = compiler.compile('
{{if bool foo bar}}
', {name: 'test/partial'}); 98 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: false})); 99 | equalTokens(dom, '
foo
', 'Inline If helper in content with else term - false'); 100 | 101 | 102 | 103 | template = compiler.compile('
test
', {name: 'test/partial'}); 104 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: true})); 105 | equalTokens(dom, '
test
', 'Inline If helper in element without else term - true'); 106 | 107 | 108 | 109 | template = compiler.compile('
test
', {name: 'test/partial'}); 110 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: true})); 111 | equalTokens(dom, '
test
', 'Inline If helper in element with else term - true'); 112 | 113 | 114 | 115 | template = compiler.compile('
test
', {name: 'test/partial'}); 116 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: false})); 117 | equalTokens(dom, '
test
', 'Inline If helper in element without else term - false'); 118 | 119 | 120 | 121 | template = compiler.compile('
test
', {name: 'test/partial'}); 122 | template.render(dom, new Model({foo:'bar', bar:'foo', bool: false})); 123 | equalTokens(dom, '
test
', 'Inline If helper in element without else term - true'); 124 | 125 | 126 | 127 | template = compiler.compile('
{{if bool foo bar}}
', {name: 'test/partial'}); 128 | data = new Model({foo:'bar', bar:'foo', bool: false}); 129 | template.render(dom, data); 130 | data.set('bool', true); 131 | equalTokens(dom, '
bar
', 'Inline If helper is data bound'); 132 | data.set('bool', '1'); 133 | equalTokens(dom, '
bar
', 'Inline If helper works with string values'); 134 | data.set('bool', ''); 135 | equalTokens(dom, '
foo
', 'Inline If helper works with falsy string values'); 136 | 137 | 138 | 139 | 140 | // Nexted Block IFs 141 | template = compiler.compile('
{{#if bool}}{{#if bool}}{{val}}{{else}}{{val2}}{{/if}}{{else}}{{val2}}{{/if}}
', {name: 'test/partial'}); 142 | data = new Model({bool: true, val: 'true', val2: 'false'}); 143 | template.render(dom, data); 144 | equal(dom.firstChild.innerHTML, 'true', 'If helpers that are the immediate children of if helpers render on first run.'); 145 | data.set('bool', false); 146 | equal(dom.firstChild.innerHTML, 'false', 'If helpers that are the immediate children of if helpers re-render successfully on change.'); 147 | 148 | // Re-eval on reset of collection 149 | template = compiler.compile('
{{if arr 1 0}}
', {name: 'test/partial'}); 150 | data = new Model({arr: []}); 151 | template.render(dom, data); 152 | data.reset({arr: [{id: 1}]}); 153 | equalTokens(dom, '
1
', 'Inline If helper is data bound for collection reset'); 154 | 155 | }); 156 | -------------------------------------------------------------------------------- /packages/rebound-compiler/test/rebound_compiler_test.js: -------------------------------------------------------------------------------- 1 | /*jslint evil: true */ 2 | 3 | import compiler from 'rebound-compiler/compile'; 4 | import parse from 'rebound-compiler/parser'; 5 | import tokenizer from 'simple-html-tokenizer'; 6 | import Model from 'rebound-data/model'; 7 | 8 | function equalTokens(fragment, html, message) { 9 | var div = document.createElement("div"); 10 | 11 | div.appendChild(fragment.cloneNode(true)); 12 | 13 | var fragTokens = tokenizer.tokenize(div.innerHTML); 14 | var htmlTokens = tokenizer.tokenize(html); 15 | 16 | function normalizeTokens(token) { 17 | if (token.type === 'StartTag') { 18 | token.attributes = token.attributes.sort(function(a,b){ 19 | if (a.name > b.name) { 20 | return 1; 21 | } 22 | if (a.name < b.name) { 23 | return -1; 24 | } 25 | return 0; 26 | }); 27 | } 28 | } 29 | 30 | fragTokens.forEach(normalizeTokens); 31 | htmlTokens.forEach(normalizeTokens); 32 | 33 | deepEqual(fragTokens, htmlTokens, message); 34 | } 35 | 36 | 37 | QUnit.test('Rebound Compiler - Partials', function( assert ) { 38 | 39 | assert.expect(11); 40 | 41 | var spec = parse('
{{foo}}
'); 42 | 43 | assert.equal(spec.isPartial, true, 'Compiler interperts plain HTMLBars strings as partials'); 44 | 45 | 46 | 47 | spec = parse(` 48 | 49 |
{{foo}}
`); 50 | 51 | assert.deepEqual(spec.deps, ['foo/bar'], 'Compiler can find a single dependancy from tag inside partials'); 52 | 53 | 54 | 55 | spec = parse(` 56 | 57 |
{{foo}}
`); 58 | 59 | assert.deepEqual(spec.deps, ['foo/bar'], 'Compiler can find a single dependancy from tag inside partials using single quotes'); 60 | 61 | 62 | 63 | spec = parse(` 64 | 65 |
{{foo}}
`); 66 | 67 | assert.deepEqual(spec.deps, ['foo/bar'], 'Compiler can find a single dependancy from tag inside partials using no quotes'); 68 | 69 | 70 | 71 | spec = parse(` 72 | 73 |
{{foo}}
`); 74 | 75 | assert.deepEqual(spec.deps, ['foo/bar'], 'Compiler is tolerant to tags having strange properties'); 76 | 77 | 78 | 79 | spec = parse(` 80 | 81 |
{{foo}}
82 | `); 83 | 84 | assert.deepEqual(spec.deps, ['foo/bar', 'far/boo'], 'Compiler can find multiple dependancies throughout partials'); 85 | assert.equal(spec.template.trim(), '
{{foo}}
', 'Compiler strips tags from partials'); 86 | 87 | 88 | 89 | spec = parse(` 90 | {{> foo/bar}} 91 |
{{foo}}
`); 92 | 93 | assert.deepEqual(spec.deps, ['foo/bar'], 'Compiler can find a single dependancy from partial handlebar tag inside partials'); 94 | 95 | 96 | 97 | spec = parse(` 98 | {{> foo/bar}} 99 |
{{foo}}
100 | {{> far/boo}}`); 101 | 102 | assert.deepEqual(spec.deps, ['foo/bar', 'far/boo'], 'Compiler can find multiple dependancies throughout partials with partial syntax'); 103 | 104 | 105 | var dom = document.createDocumentFragment(); 106 | var template = compiler.compile('
{{foo}}
', {name:'test/partial'}); 107 | template.render(dom, new Model({foo:'bar', bar:'foo'})); 108 | equalTokens(dom.firstChild, '
bar
', 'Compiler accepts plain HTMLBars strings and returns working template'); 109 | 110 | template = compiler.compile('{{partial "test/partial"}}', {name:'test'}); 111 | template.render(dom, new Model({foo:'bar', bar:'foo'})); 112 | // In PhantomJS, document fragments don't have a firstElementChild property 113 | equalTokens(dom.childNodes[1], '
bar
', 'Compiler registers partial for use in other templates'); 114 | 115 | 116 | }); 117 | 118 | 119 | 120 | 121 | 122 | 123 | QUnit.test('Rebound Compiler - Components', function( assert ) { 124 | 125 | assert.expect(14); 126 | 127 | var spec = parse(` 128 | 129 | 133 | 138 | `); 139 | 140 | assert.equal(spec.isPartial, false, 'Compiler interperts component templates as components'); 141 | assert.equal(spec.name, 'test-element', 'Compiler extracts name from element'); 142 | assert.deepEqual(spec.deps, ['foo/bar'], 'Compiler can find a single dependancy from tag inside components'); 143 | assert.equal(spec.template.trim(), '
{{foo}}
', 'Compiler strips tags from partials'); 144 | assert.deepEqual(eval(spec.script), {foo: 'bar'}, 'Script inside of element evals properly'); 145 | 146 | 147 | 148 | spec = parse(` 149 | 150 | 156 | 157 | `); 158 | 159 | assert.equal(spec.name, 'test-element', 'Compiler extracts name from element with single quotes'); 160 | assert.equal(spec.name, 'test-element', 'Compiler extracts name from element with other properties on the element tag'); 161 | assert.deepEqual(spec.deps, ['foo/bar', 'bar/foo', 'far/boo'], 'Compiler can find a multiple dependancies from both tags and partials inside components'); 162 | assert.deepEqual(eval(spec.script), undefined, 'Empty script inside of element evals properly'); 163 | 164 | 165 | 166 | spec = parse(` 167 | 168 | 169 | `); 170 | 171 | assert.equal(spec.name, 'test-element', 'Compiler extracts name from element with no quotes'); 172 | assert.equal(spec.name, 'test-element', 'Compiler extracts the last name property from element with single quotes'); 173 | assert.deepEqual(spec.template, '', 'Compiler works with empty template tag'); 174 | assert.deepEqual(eval(spec.script), undefined, 'No script inside of element evals properly'); 175 | 176 | 177 | 178 | spec = parse(``); 179 | 180 | assert.deepEqual(spec.template, '', 'Compiler works with no template tag'); 181 | 182 | 183 | var template = compiler.compile(` 184 | 185 | 189 | 194 | `); 195 | 196 | // var el = document.createElement('test-element'); 197 | // 198 | // equal(el.data.isComponent, true, 'Compiler registers new element for use'); 199 | 200 | }); 201 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/lib/render.js: -------------------------------------------------------------------------------- 1 | import { $, REBOUND_SYMBOL } from "rebound-utils/rebound-utils"; 2 | import _hooks from "rebound-htmlbars/hooks"; 3 | 4 | var RENDER_TIMEOUT; 5 | var TO_RENDER = []; 6 | var ENV_QUEUE = []; 7 | 8 | 9 | // A convenience method to push only unique eleents in an array of objects to 10 | // the TO_RENDER queue. If the element is a Lazy Value, it marks it as dirty in 11 | // the process 12 | var push = function(arr){ 13 | var i, len = arr.length; 14 | this.added || (this.added = {}); 15 | arr.forEach((item) => { 16 | if(this.added[item.cid]){ return; } 17 | this.added[item.cid] = 1; 18 | if(item.isLazyValue){ item.makeDirty(); } 19 | this.push(item); 20 | }); 21 | }; 22 | 23 | function reslot(env){ 24 | 25 | // Fix for stupid Babel module importer 26 | // TODO: Fix this. This is dumb. Modules don't resolve in by time of this file's 27 | // execution because of the dependancy tree so babel doesn't get a chance to 28 | // interop the default value of these imports. We need to do this at runtime instead. 29 | var hooks = _hooks.default || _hooks; 30 | 31 | var outlet, 32 | slots = env.root.options && env.root.options[REBOUND_SYMBOL]; 33 | 34 | if(!env.root || !slots){ return; } 35 | 36 | // Walk the dom, without traversing into other custom elements, and search for 37 | // `` outlets to render templates into. 38 | $(env.root.el).walkTheDOM(function(el){ 39 | if(env.root.el === el){ return true; } 40 | if(el.tagName === 'CONTENT'){ outlet = el; } 41 | if(el.tagName.indexOf('-') > -1){ return false; } 42 | return true; 43 | }); 44 | 45 | // If a `` outlet is present in component's template, and a template 46 | // is provided, render it into the outlet 47 | if(slots.templates.default && _.isElement(outlet) && !outlet.slotted){ 48 | outlet.slotted = true; 49 | $(outlet).empty(); 50 | outlet.appendChild(hooks.buildRenderResult(slots.templates.default, slots.env, slots.scope, {}).fragment); 51 | } 52 | } 53 | 54 | // Called on animation frame. TO_RENDER is a list of lazy-values to notify. 55 | // When notified, they mark themselves as dirty. Then, call revalidate on all 56 | // dirty expressions for each environment we need to re-render. Use `while(queue.length)` 57 | // to accomodate synchronous renders where the render queue callbacks may trigger 58 | // nested calls of `renderCallback`. 59 | function renderCallback(){ 60 | 61 | while(TO_RENDER.length){ 62 | TO_RENDER.shift().notify(); 63 | } 64 | 65 | TO_RENDER.added = {}; 66 | 67 | while(ENV_QUEUE.length){ 68 | let env = ENV_QUEUE.shift(); 69 | for(let key in env.revalidateQueue){ 70 | env.revalidateQueue[key].revalidate(); 71 | } 72 | reslot(env); 73 | } 74 | ENV_QUEUE.added = {}; 75 | } 76 | 77 | // Listens for `change` events and calls `trigger` with the correct values 78 | function onChange(model, options){ 79 | trigger.call(this, 'change', model, model.changedAttributes()); 80 | } 81 | 82 | // Listens for `reset` events and calls `trigger` with the correct values 83 | function onReset(data, options){ 84 | trigger.call(this, 'reset', data, data.isModel ? data.changedAttributes() : { '@each': data }, options); 85 | } 86 | 87 | // Listens for `update` events and calls `trigger` with the correct values 88 | function onUpdate(collection, options){ 89 | trigger.call(this, 'update', collection, { '@each': collection }, options); 90 | } 91 | 92 | 93 | function trigger(type, data, changed, options={}){ 94 | 95 | // If nothing has changed, exit. 96 | if(!data || !changed){ return void 0; } 97 | 98 | var basePath = data.__path(); 99 | 100 | // If this event came from within a service, include the service key in the base path 101 | if(options.service){ basePath = options.service + '.' + basePath; } 102 | 103 | // For each changed key, walk down the data tree from the root to the data 104 | // element that triggered the event and add all relevent callbacks to this 105 | // object's TO_RENDER queue. 106 | basePath = basePath.replace(/\[[^\]]+\]/g, ".@each"); 107 | var parts = $.splitPath(basePath); 108 | var context = []; 109 | 110 | while(1){ 111 | let pre = context.join('.').trim(); 112 | let post = parts.join('.').trim(); 113 | 114 | for(let key in changed){ 115 | let path = (post + (post && key && '.') + key).trim(); 116 | for(let testPath in this.env.observers[pre]){ 117 | if($.startsWith(testPath, path)){ 118 | push.call(TO_RENDER, this.env.observers[pre][testPath]); 119 | push.call(ENV_QUEUE, [this.env]); 120 | } 121 | } 122 | } 123 | if(parts.length === 0){ break; } 124 | context.push(parts.shift()); 125 | } 126 | 127 | // If Rebound is loaded in a testing environment, call renderCallback syncronously 128 | // so that changes to the data reflect in the DOM immediately. 129 | // TODO: Make tests async so this is not required 130 | if(window.Rebound && window.Rebound.testing){ return renderCallback(); } 131 | 132 | // Otherwise, queue our render callback to be called on the next animation frame, 133 | // after the current call stack has been exhausted. 134 | window.cancelAnimationFrame(RENDER_TIMEOUT); 135 | RENDER_TIMEOUT = window.requestAnimationFrame(renderCallback); 136 | } 137 | 138 | 139 | // A render function that will merge user provided helpers and hooks with our defaults 140 | // and bind a method that re-renders dirty expressions on data change and executes 141 | // other delegated listeners added by our hooks. 142 | export default function render(el, template, data, options={}){ 143 | 144 | // Fix for stupid Babel module importer 145 | // TODO: Fix this. This is dumb. Modules don't resolve in by time of this file's 146 | // execution because of the dependancy tree so babel doesn't get a chance to 147 | // interop the default value of these imports. We need to do this at runtime instead. 148 | var hooks = _hooks.default || _hooks; 149 | 150 | // If no data is passed to render, exit with an error 151 | if(!data){ return console.error('No data passed to render function.'); } 152 | 153 | // Every component's template is rendered using a unique Environment and Scope 154 | // If this component already has them, re-use the same objects – they contain 155 | // important state information. Otherwise, create fresh ones for it. 156 | var env = data.env || hooks.createFreshEnv(); 157 | var scope = data.scope || hooks.createFreshScope(); 158 | 159 | // Bind the component as the scope's main data object 160 | hooks.bindSelf(env, scope, data); 161 | 162 | // Add template specific hepers to env 163 | _.extend(env.helpers, options.helpers); 164 | 165 | // Save env and scope on component data to trigger lazy-value streams on data change 166 | data.env = env; 167 | data.scope = scope; 168 | 169 | // Save data on env to allow helpers / hooks access to component methods 170 | env.root = data; 171 | 172 | // Ensure we have a contextual element to pass to render 173 | options.contextualElement || (options.contextualElement = (data.el || document.body)); 174 | options.self = data; 175 | 176 | // If data is an eventable object, run the onChange helper on any change 177 | if(data.listenTo){ 178 | data.stopListening(null, null, onChange).stopListening(null, null, onReset).stopListening(null, null, onUpdate); 179 | data.listenTo(data, 'change', onChange).listenTo(data, 'reset', onReset).listenTo(data, 'update', onUpdate); 180 | } 181 | 182 | // If this is a real template, run it with our merged helpers and hooks 183 | // If there is no template, just return an empty fragment 184 | env.template = template ? hooks.buildRenderResult(template, env, scope, options) : { fragment: document.createDocumentFragment() }; 185 | $(el).empty(); 186 | el.appendChild(env.template.fragment); 187 | reslot(env); 188 | return el; 189 | } 190 | -------------------------------------------------------------------------------- /packages/property-compiler/test/property_compiler_test.js: -------------------------------------------------------------------------------- 1 | import compiler from "property-compiler/property-compiler"; 2 | 3 | QUnit.test('Rebound Property Compiler', function() { 4 | 5 | var func, res; 6 | 7 | func = function(){ 8 | return 1; 9 | }; 10 | res = compiler.compile(func, 'path'); 11 | deepEqual( res, [], 'Property Compiler returns empty array if no data is accessed' ); 12 | 13 | 14 | 15 | func = function(){ 16 | return this.get('test'); 17 | }; 18 | res = compiler.compile(func, 'path'); 19 | deepEqual( res, ['test'], 'Property Compiler returns proper dependancy for single get' ); 20 | 21 | 22 | 23 | func = function(){ 24 | return this.get('test.more'); 25 | }; 26 | res = compiler.compile(func, 'path'); 27 | deepEqual( res, ['test.more'], 'Property Compiler returns proper dependancy for complex single get' ); 28 | 29 | 30 | 31 | func = function(){ 32 | return this.get('test.more').get('again.foo').get('bar'); 33 | }; 34 | res = compiler.compile(func, 'path'); 35 | deepEqual( res, ['test.more.again.foo.bar'], 'Property Compiler returns proper dependancy for complex chained gets' ); 36 | 37 | 38 | 39 | func = function(){ 40 | return this.at(1); 41 | }; 42 | res = compiler.compile(func, 'path'); 43 | deepEqual( res, ['@each'], 'Property Compiler returns proper dependancy for root level at()' ); 44 | 45 | 46 | 47 | func = function(){ 48 | return this.get('test[1].more'); 49 | }; 50 | res = compiler.compile(func, 'path'); 51 | deepEqual( res, ['test.@each.more'], 'Property Compiler returns proper dependancy for get including array referance' ); 52 | 53 | 54 | 55 | func = function(){ 56 | return this.get('test.more').at(1).get('again.foo'); 57 | }; 58 | res = compiler.compile(func, 'path'); 59 | deepEqual( res, ['test.more.@each.again.foo'], 'Property Compiler returns proper dependancy for chained gets and at()' ); 60 | 61 | 62 | 63 | func = function(){ 64 | return this.get('test.more').at(1).get('again.foo'); 65 | }; 66 | res = compiler.compile(func, 'path'); 67 | deepEqual( res, ['test.more.@each.again.foo'], 'Property Compiler returns proper dependancy for chained gets and at()' ); 68 | 69 | 70 | 71 | func = function(){ 72 | return this.get('test.more').get('andMore').where({test : 1}); 73 | }; 74 | 75 | res = compiler.compile(func, 'path'); 76 | deepEqual( res, ['test.more.andMore.@each.test'], 'Property Compiler returns proper dependancy for chained gets and where() with single argument' ); 77 | 78 | 79 | 80 | func = function(){ 81 | return this.get('test.more').get('andMore').where({test : 1, bar: 'foo'}); 82 | }; 83 | res = compiler.compile(func, 'path'); 84 | deepEqual( res, ['test.more.andMore.@each.test', 'test.more.andMore.@each.bar'], 'Property Compiler returns proper dependancy for chained gets and where() with multiple arguments' ); 85 | 86 | 87 | 88 | func = function(){ 89 | return this.get('test.more').findWhere({test : 1}); 90 | }; 91 | res = compiler.compile(func, 'path'); 92 | deepEqual( res, ['test.more.@each.test'], 'Property Compiler returns proper dependancy for chained gets and findWhere() with single argument' ); 93 | 94 | 95 | 96 | 97 | func = function(){ 98 | return this.get('test.more').findWhere({test : 1, bar: 'foo'}); 99 | }; 100 | res = compiler.compile(func, 'path'); 101 | deepEqual( res, ['test.more.@each.test', 'test.more.@each.bar'], 'Property Compiler returns proper dependancy for chained gets and findWhere() with multiple arguments' ); 102 | 103 | 104 | 105 | 106 | func = function(){ 107 | return this.get('test.more').pluck('test'); 108 | }; 109 | res = compiler.compile(func, 'path'); 110 | deepEqual( res, ['test.more.@each.test'], 'Property Compiler returns proper dependancy for chained gets and pluck()' ); 111 | 112 | 113 | 114 | func = function(){ 115 | return this.get('test.more').slice(0,3); 116 | }; 117 | res = compiler.compile(func, 'path'); 118 | deepEqual( res, ['test.more.@each'], 'Property Compiler returns proper dependancy for chained gets and slice()' ); 119 | 120 | 121 | 122 | func = function(){ 123 | return this.get('test.more').clone(); 124 | }; 125 | res = compiler.compile(func, 'path'); 126 | deepEqual( res, ['test.more.@each'], 'Property Compiler returns proper dependancy for chained gets and clone()' ); 127 | 128 | 129 | 130 | func = function(){ 131 | // This shouldn't break anything 132 | return this.get('test'); 133 | }; 134 | res = compiler.compile(func, 'path'); 135 | deepEqual( res, ['test'], 'Property Compiler ignores single line comments' ); 136 | 137 | 138 | 139 | func = function(){ 140 | /* 141 | This 142 | shouldn't 143 | break 144 | anything 145 | */ 146 | return this.get('test'); 147 | }; 148 | res = compiler.compile(func, 'path'); 149 | deepEqual( res, ['test'], 'Property Compiler ignores multiline comments' ); 150 | 151 | func = function(){ 152 | if(this.get('one') === 'login' && this.get('two')){ 153 | return 1; 154 | } 155 | return 0; 156 | }; 157 | res = compiler.compile(func, 'path'); 158 | deepEqual( res, ['one', 'two'], 'Property Compiler works with complex if statement (multiple terminators between `this`)' ); 159 | 160 | 161 | func = function(){ 162 | if(this.get('page') === 'login' && this.get('user.uid')){ 163 | this.set('page', 'checkout'); 164 | return 1; 165 | } 166 | return 0; 167 | }; 168 | 169 | res = compiler.compile(func, 'path'); 170 | deepEqual( res, ['page', 'user.uid'], 'Property Compiler works with complex if statement (multiple terminators between `this`)' ); 171 | 172 | 173 | /******************************* 174 | ES6 175 | ********************************/ 176 | 177 | 178 | func = function(){ 179 | var res; 180 | if(true){ 181 | let a = this.get('test'); 182 | res = a; 183 | } 184 | return res; 185 | }; 186 | res = compiler.compile(func, 'path'); 187 | deepEqual( res, ['test'], 'Block scoped variables dont prevent dependancy discovery' ); 188 | 189 | 190 | // func = function(){ 191 | // let a = () => { return this.get('test'); } 192 | // return a(); 193 | // }; 194 | // 195 | // res = compiler.compile(func, 'path'); 196 | // deepEqual( res, ['test'], 'Arrow functions dont prevent dependancy discovery' ); 197 | 198 | 199 | 200 | // TODO: Features to eventually support 201 | // 202 | // 203 | // func = function(){ 204 | // var foo = this.get('foo'); 205 | // return foo.get('bar'); 206 | // }; 207 | // compiler.register({cid: 'testId'}, 'key', func, 'path'); 208 | // res = compiler.compile(func, 'path'); 209 | // deepEqual( res, ['foo.bar'], 'Property Compiler saves state when object is saved to a variable' ); 210 | // 211 | // 212 | // 213 | // func = function(){ 214 | // return this.get('foo').get(this.get('test')); 215 | // }; 216 | // compiler.register({cid: 'testId'}, 'key', func, 'path'); 217 | // res = compiler.compile(func, 'path'); 218 | // deepEqual( res, ['foo.@each', 'test'], 'Property Compiler returns proper dependancy for nested gets' ); 219 | // 220 | // 221 | // 222 | // func = function(){ 223 | // return this.get('foo').at(this.get('test')); 224 | // }; 225 | // compiler.register({cid: 'testId'}, 'key', func, 'path'); 226 | // res = compiler.compile(func, 'path'); 227 | // deepEqual( res, ['foo.@each', 'test'], 'Property Compiler returns proper dependancy for nested at' ); 228 | // 229 | // 230 | // 231 | // func = function(){ 232 | // var that = this; 233 | // return that.get('foo.bar'); 234 | // }; 235 | // compiler.register({cid: 'testId'}, 'key', func, 'path'); 236 | // res = compiler.compile(func, 'path'); 237 | // deepEqual( res, ['foo.bar'], 'Property Compiler can handle ailiased `this` varialbe' ); 238 | // 239 | // 240 | 241 | }); 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /packages/rebound-component/test/rebound_services_test.js: -------------------------------------------------------------------------------- 1 | import compiler from 'rebound-compiler/compile'; 2 | import tokenizer from 'simple-html-tokenizer'; 3 | import Component from 'rebound-component/component'; 4 | 5 | QUnit.test('Rebound Services', function() { 6 | 7 | var service = new (Component.extend({ 8 | int: 1, 9 | arr: [{a:1, b:2, c:3}], 10 | obj: {d:4, e:5, f:6} 11 | })); 12 | 13 | var service2 = new (Component.extend({ 14 | foo: 'bar' 15 | })); 16 | 17 | var component1 = Component.extend({ 18 | service: service, 19 | service2: service2, 20 | 21 | get attributeProxy(){ 22 | return this.get('service.int'); 23 | }, 24 | get deepObjectAttributeProxy(){ 25 | return this.get('service.obj.f'); 26 | }, 27 | get deepArrayAttributeProxy(){ 28 | return this.get('service.arr[0].b'); 29 | }, 30 | get arrayProxy(){ 31 | return this.get('service.arr'); 32 | }, 33 | get objectProxy(){ 34 | return this.get('service.obj'); 35 | } 36 | }); 37 | var component2 = Component.extend({ 38 | service: service, 39 | service2: service2, 40 | 41 | get attributeProxy(){ 42 | return this.get('service.int'); 43 | }, 44 | get deepObjectAttributeProxy(){ 45 | return this.get('service.obj.f'); 46 | }, 47 | get deepArrayAttributeProxy(){ 48 | return this.get('service.arr[0].b'); 49 | }, 50 | get arrayProxy(){ 51 | return this.get('service.arr'); 52 | }, 53 | get objectProxy(){ 54 | return this.get('service.obj'); 55 | } 56 | }); 57 | 58 | var instance1 = new component1(); 59 | var instance2 = new component2(); 60 | 61 | // Inherit top level properties 62 | equal(service.get('int'), instance1.get('service.int'), 'Instances of components inheriting services get top level properties.'); 63 | equal(service.get('int'), instance2.get('service.int'), 'Multiple components can inherit services and get top level properties.'); 64 | 65 | // Inherit deep properties 66 | equal(service.get('obj.f'), instance1.get('service.obj.f'), 'Instances of components inheriting services get deep properties inside objects.'); 67 | equal(service.get('obj.f'), instance2.get('service.obj.f'), 'Multiple components can inherit services and get deep properties inside objects.'); 68 | 69 | // Inherit properties through collections 70 | equal(service.get('arr[0].b'), instance1.get('service.arr[0].b'), 'Instances of components inheriting services get properties through collections.'); 71 | equal(service.get('arr[0].b'), instance2.get('service.arr[0].b'), 'Multiple components can inherit services and get properties through collections.'); 72 | 73 | // Computed properties - Inherited attributes and object proxies 74 | equal(service.get('int'), instance1.get('attributeProxy'), 'Computed properties can depend on inherited services for top level service attributes.'); 75 | equal(service.get('obj.f'), instance1.get('deepObjectAttributeProxy'), 'Computed properties can depend on inherited services for deep attributes.'); 76 | equal(service.get('arr[0].b'), instance1.get('deepArrayAttributeProxy'), 'Computed properties can depend on inherited services for deep attributes through arrays.'); 77 | deepEqual(service.get('arr').toJSON(), instance1.get('arrayProxy').toJSON(), 'Computed properties can proxy collection inside inherited services.'); 78 | deepEqual(service.get('obj').toJSON(), instance1.get('objectProxy').toJSON(), 'Computed properties can proxy object inside inherited services.'); 79 | 80 | service.set('int', 1); 81 | 82 | // Property recomputes 83 | equal(service.get('int'), instance1.get('service.int'), 'Changes to top level service properties are reflected in components.'); 84 | equal(service.get('int'), instance2.get('service.int'), 'Changes to top level service properties are reflected in multiple components.'); 85 | equal(service.get('int'), instance1.get('attributeProxy'), 'Computed properties depending on top level service properties re-compute on change.'); 86 | equal(service.get('int'), instance2.get('attributeProxy'), 'Computed properties in multiple components depending on top level service properties re-compute on change.'); 87 | 88 | service.set('obj.f', 7); 89 | 90 | // Object recomputes 91 | equal(service.get('obj.f'), instance1.get('service.obj.f'), 'Changes to deeply nested service properties are reflected in components.'); 92 | equal(service.get('obj.f'), instance2.get('service.obj.f'), 'Changes to deeply nested service properties are reflected in multiple components.'); 93 | equal(service.get('obj.f'), instance1.get('deepObjectAttributeProxy'), 'Computed properties depending on deeply nested service properties re-compute on change.'); 94 | equal(service.get('obj.f'), instance2.get('deepObjectAttributeProxy'), 'Computed properties in multiple components depending on deeply nested service properties re-compute on change.'); 95 | 96 | service.set('arr[0].b', 3); 97 | 98 | // Property recomputations through arrays 99 | equal(service.get('arr[0].b'), instance1.get('service.arr[0].b'), 'Changes to deeply nested service properties are reflected in components.'); 100 | equal(service.get('arr[0].b'), instance2.get('service.arr[0].b'), 'Changes to deeply nested service properties are reflected in multiple components.'); 101 | equal(service.get('arr[0].b'), instance1.get('deepArrayAttributeProxy'), 'Computed properties depending on deeply nested service properties through collections re-compute on change.'); 102 | equal(service.get('arr[0].b'), instance2.get('deepArrayAttributeProxy'), 'Computed properties in multiple components depending on deeply nested service properties through collections re-compute on change.'); 103 | 104 | service.set('obj.g', 8); 105 | 106 | // Object modification recomutes 107 | equal(service.get('obj.g'), instance1.get('service.obj.g'), 'Key additions to deeply nested service properties are reflected in components.'); 108 | equal(service.get('obj.g'), instance2.get('service.obj.g'), 'Key additions to deeply nested service properties are reflected in multiple components.'); 109 | deepEqual(service.get('obj').toJSON(), instance1.get('objectProxy').toJSON(), 'Computed properties proxying deeply nested service objects re-compute on key additions.'); 110 | deepEqual(service.get('obj').toJSON(), instance2.get('objectProxy').toJSON(), 'Computed properties in multiple components proxying deeply nested service properties re-compute on key additions.'); 111 | 112 | service.get('arr').add({foo: 'bar'}); 113 | 114 | // Array addtion recomputes 115 | deepEqual(service.get('arr').toJSON(), instance1.get('arrayProxy').toJSON(), 'Computed properties proxying collections in a service re-compute on model additions.'); 116 | deepEqual(service.get('arr').toJSON(), instance2.get('arrayProxy').toJSON(), 'Computed properties in multiple components proxying collections in a service re-compute on model additions.'); 117 | 118 | service.get('arr').pop(); 119 | 120 | // Array removal recomputes 121 | deepEqual(service.get('arr').toJSON(), instance1.get('arrayProxy').toJSON(), 'Computed properties proxying collections in a service re-compute on model removals.'); 122 | deepEqual(service.get('arr').toJSON(), instance2.get('arrayProxy').toJSON(), 'Computed properties in multiple components proxying collections in a service re-compute on model removals.'); 123 | 124 | // Inheritance from multiple services 125 | equal(service2.get('foo'), instance1.get('service2.foo'), 'A component can inherit from multiple services.'); 126 | equal(service2.get('foo'), instance2.get('service2.foo'), 'Multiple components can inherit from multiple shared services.'); 127 | 128 | instance2.deinitialize(); 129 | 130 | equal(instance1.get('service2.foo'), 'bar', 'Services continue to persist even after consuming object deinitialization.'); 131 | 132 | instance1.set('service2.foo', 'foo'); 133 | instance1.reset(); 134 | equal(instance1.get('service2.foo'), 'foo', 'Services are unaffected by comsuming objects\' reset events.'); 135 | 136 | 137 | 138 | }); 139 | 140 | // Components pass default settings to child models and are reset propery on reset() 141 | -------------------------------------------------------------------------------- /packages/rebound-data/lib/collection.js: -------------------------------------------------------------------------------- 1 | // Rebound Collection 2 | // ---------------- 3 | 4 | import Backbone from "backbone"; 5 | import Model from "rebound-data/model"; 6 | import $ from "rebound-utils/rebound-utils"; 7 | 8 | function pathGenerator(collection){ 9 | return function(){ 10 | return collection.__path() + '[' + collection.indexOf(collection._byId[this.cid]) + ']'; 11 | }; 12 | } 13 | 14 | var Collection = Backbone.Collection.extend({ 15 | 16 | isCollection: true, 17 | isData: true, 18 | 19 | model: Model, 20 | 21 | __path: function(){return '';}, 22 | 23 | constructor: function(models, options){ 24 | models || (models = []); 25 | options || (options = {}); 26 | this._byValue = {}; 27 | this.helpers = {}; 28 | this.cid = $.uniqueId('collection'); 29 | 30 | // Set lineage 31 | this.setParent( options.parent || this ); 32 | this.setRoot( options.root || this ); 33 | this.__path = options.path || this.__path; 34 | 35 | Backbone.Collection.apply( this, arguments ); 36 | 37 | // When a model is removed from its original collection, destroy it 38 | // TODO: Fix this. Computed properties now somehow allow collection to share a model. They may be removed from one but not the other. That is bad. 39 | // The clone = false options is the culprit. Find a better way to copy all of the collections custom attributes over to the clone. 40 | this.on('remove', function(model, collection, options){ 41 | // model.deinitialize(); 42 | }); 43 | 44 | }, 45 | 46 | // TODO: Start - `Upstream to Backbone?`. 47 | // Always give precedence to the provided model's idAttribute. Fall back to 48 | // the Collection's idAttribute, and then to the default `id`. 49 | modelId: function(model={}, data={}){ 50 | // Always give precedence to the provided model's idAttribute. Fall back to 51 | // the Collection's idAttribute, and then to the default `id`. 52 | var idAttribute = model.idAttribute || this.model.prototype.idAttribute || 'id'; 53 | 54 | // If this is a data element, just return the id 55 | if(data.isData){ return data.get(idAttribute); } 56 | 57 | // Otherwise, iterate down the object trying to get the id 58 | $.splitPath(idAttribute).forEach(function(val, key){ 59 | if(!_.isObject(data)){ return; } 60 | data = data.isData ? data.get(val) : data[val]; 61 | }); 62 | 63 | return data; 64 | }, 65 | 66 | // Pass modelId the model itself, not just the attributes, so it can get the 67 | // idAttribute from the model itslef and not the collection 68 | _addReference: function(model, options) { 69 | this._byId[model.cid] = model; 70 | var id = this.modelId(model, model); 71 | if (id != null){ this._byId[id] = model; } 72 | model.on('all', this._onModelEvent, this); 73 | }, 74 | 75 | // Pass modelId the model itself, not just the attributes, so it can get the 76 | // idAttribute from the model itslef and not the collection 77 | _removeReference: function(model, options) { 78 | delete this._byId[model.cid]; 79 | var id = this.modelId(model, model); 80 | if (id != null){ delete this._byId[id]; } 81 | if (this === model.collection){ delete model.collection; } 82 | model.off('all', this._onModelEvent, this); 83 | }, 84 | 85 | // Pass modelId the model itself, not just the attributes, so it can get the 86 | // idAttribute from the model itslef and not the collection 87 | _onModelEvent: function(event, model, collection, options) { 88 | if ((event === 'add' || event === 'remove') && collection !== this) return; 89 | if (event === 'destroy') this.remove(model, options); 90 | if (event === 'change') { 91 | var prevId = this.modelId(model, model.previousAttributes()); 92 | var id = this.modelId(model, model); 93 | if (prevId !== id) { 94 | if (prevId != null){ delete this._byId[prevId]; } 95 | if (id != null){ this._byId[id] = model; } 96 | } 97 | } 98 | this.trigger.apply(this, arguments); 99 | }, 100 | // TODO: End - `Upstream to Backbone?`. 101 | 102 | 103 | get: function(key, options){ 104 | 105 | // Split the path at all '.', '[' and ']' and find the value referanced. 106 | var parts = _.isString(key) ? $.splitPath(key) : [], 107 | result = this, 108 | l=parts.length, 109 | i=0; 110 | options || (options = {}); 111 | 112 | // If the key is a number or object, or just a single string that is not a path, 113 | // get by id and return the first occurance 114 | if(typeof key == 'number' || typeof key == 'object' || (parts.length == 1 && !options.isPath)){ 115 | if (key === null){ return void 0; } 116 | var id = this.modelId(key, key); 117 | var responses = [].concat(this._byValue[key], (this._byId[key] || this._byId[id] || this._byId[key.cid])); 118 | var res = responses[0], idx = Infinity; 119 | 120 | responses.forEach((value) => { 121 | if(!value){ return void 0; } 122 | let i = _.indexOf(this.models, value); 123 | if(i > -1 && i < idx){ idx = i; res = value;} 124 | }); 125 | 126 | return res; 127 | } 128 | 129 | // If key is not a string, return undefined 130 | if (!_.isString(key)){ return void 0; } 131 | 132 | if(_.isUndefined(key) || _.isNull(key)){ return key; } 133 | if(key === '' || parts.length === 0){ return result; } 134 | 135 | if (parts.length > 0) { 136 | for ( i = 0; i < l; i++) { 137 | // If returning raw, always return the first computed property found. If undefined, you're done. 138 | if(result && result.isComputedProperty && options.raw) return result; 139 | if(result && result.isComputedProperty) result = result.value(); 140 | if(_.isUndefined(result) || _.isNull(result)) return result; 141 | if(parts[i] === '@parent') result = result.__parent__; 142 | else if(result.isCollection) result = result.models[parts[i]]; 143 | else if(result.isModel) result = result.attributes[parts[i]]; 144 | else if(result.hasOwnProperty(parts[i])) result = result[parts[i]]; 145 | } 146 | } 147 | 148 | if(result && result.isComputedProperty && !options.raw) result = result.value(); 149 | 150 | return result; 151 | }, 152 | 153 | set: function(models, options){ 154 | var newModels = [], 155 | parts = _.isString(models) ? $.splitPath(models) : [], 156 | res, 157 | lineage = { 158 | parent: this, 159 | root: this.__root__, 160 | path: pathGenerator(this), 161 | silent: true 162 | }; 163 | options = options || {}, 164 | 165 | // If no models passed, implies an empty array 166 | models || (models = []); 167 | 168 | // If models is a string, and it has parts, call set at that path 169 | if(_.isString(models) && parts.length > 1 && !isNaN(Number(parts[0]))){ 170 | let index = Number(parts[0]); 171 | return this.at(index).set(parts.splice(1, parts.length).join('.'), options); 172 | } 173 | 174 | // If another collection, treat like an array 175 | models = (models.isCollection) ? models.models : models; 176 | // Ensure models is an array 177 | models = (!_.isArray(models)) ? [models] : models; 178 | 179 | // If the model already exists in this collection, or we are told not to clone it, let Backbone handle the merge 180 | // Otherwise, create our copy of this model, give them the same cid so our helpers treat them as the same object 181 | // Use the more unique of the two constructors. If our Model has a custom constructor, use that. Otherwise, use 182 | // Collection default Model constructor. 183 | _.each(models, function(data, index){ 184 | if(data.isModel && options.clone === false || this._byId[data.cid]) return newModels[index] = data; 185 | var constructor = (data.constructor !== Object && data.constructor !== Rebound.Model) ? data.constructor : this.model; 186 | newModels[index] = new constructor(data, _.defaults(lineage, options)); 187 | data.isModel && (newModels[index].cid = data.cid); 188 | }, this); 189 | 190 | // Ensure that this element now knows that it has children now. Without this cyclic dependancies cause issues 191 | this._hasAncestry || (this._hasAncestry = newModels.length > 0); 192 | 193 | // Call original set function with model duplicates 194 | return Backbone.Collection.prototype.set.call(this, newModels, options); 195 | 196 | } 197 | 198 | }); 199 | 200 | export default Collection; 201 | -------------------------------------------------------------------------------- /packages/rebound-htmlbars/test/rebound_helpers_attribute_test.js: -------------------------------------------------------------------------------- 1 | import compiler from 'rebound-compiler/compile'; 2 | import tokenizer from 'simple-html-tokenizer'; 3 | import helpers from 'rebound-htmlbars/helpers'; 4 | import Model from 'rebound-data/model'; 5 | 6 | function equalTokens(fragment, html, message) { 7 | var div = document.createElement("div"); 8 | 9 | div.appendChild(fragment.cloneNode(true)); 10 | 11 | var fragTokens = tokenizer.tokenize(div.innerHTML); 12 | var htmlTokens = tokenizer.tokenize(html); 13 | 14 | function normalizeTokens(token) { 15 | if (token.type === 'StartTag') { 16 | token.attributes = token.attributes.sort(function(a, b) { 17 | // IE9 does strange things with uppercasing checkboxes' checked property 18 | a[0] = a[0] ? a[0].toLowerCase() : a[0]; 19 | b[0] = b[0] ? b[0].toLowerCase() : b[0]; 20 | if (a[0] > b[0]) { 21 | return 1; 22 | } 23 | if (a[0] < b[0]) { 24 | return -1; 25 | } 26 | return 0; 27 | }); 28 | } 29 | } 30 | 31 | fragTokens.forEach(normalizeTokens); 32 | htmlTokens.forEach(normalizeTokens); 33 | 34 | deepEqual(fragTokens, htmlTokens, message); 35 | } 36 | 37 | /************************************************************ 38 | 39 | Attribute 40 | 41 | *************************************************************/ 42 | 43 | QUnit.test('Rebound Helpers - Attribute', function() { 44 | 45 | 46 | /*******************************************************************/ 47 | /** The only interface these helpers should need is get and set. **/ 48 | /** Augment the object prototype to provide this api **/ 49 | 50 | // Object.prototype.get = function(key){ return this[key]; }; 51 | // Object.prototype.set = function(key, val){ this[key] = val; }; 52 | 53 | /*******************************************************************/ 54 | 55 | var evt = document.createEvent("HTMLEvents"); 56 | evt.initEvent("change", false, true); 57 | 58 | var template, data, dom = document.createDocumentFragment(); 59 | 60 | template = compiler.compile('
test
', {name: 'test/partial'}); 61 | template.render(dom, new Model({foo:'bar', bar:'foo'})); 62 | equalTokens(dom, '
test
', 'Attribute helper adds element attribute'); 63 | 64 | 65 | 66 | template = compiler.compile('
test
', {name: 'test/partial'}); 67 | window.foo = true; 68 | template.render(dom, new Model({foo:'bar', bar:'foo'})); 69 | equalTokens(dom, '
test
', 'Attribute helper appends additional element attributes'); 70 | 71 | 72 | 73 | template = compiler.compile('
test
', {name: 'test/partial'}); 74 | data = new Model({foo:'bar', bar:'foo', bool: false}); 75 | template.render(dom, data); 76 | data.set('bar', 'bar'); 77 | equalTokens(dom, '
test
', 'Attribute is data bound'); 78 | 79 | 80 | 81 | template = compiler.compile('', {name: 'test/partial'}); 82 | data = new Model({foo:'bar', bar:'foo', bool: false}); 83 | template.render(dom, data); 84 | data.set('bar', 'bar'); 85 | equal(dom.firstChild.value, 'bar', 'Value of text input is two way data bound data -> element'); 86 | equalTokens(dom, '', 'Value Attribute on text input is two way data bound element -> data'); 87 | dom.firstChild.value = 'Hello World'; 88 | dom.firstChild.dispatchEvent(evt); 89 | equal(data.get('bar'), 'Hello World', 'Value on text input is two way data bound element -> data'); 90 | 91 | 92 | 93 | template = compiler.compile('', {name: 'test/partial'}); 94 | data = new Model({foo:'bar', bar:'foo', bool: false}); 95 | template.render(dom, data); 96 | data.set('bar', 'bar'); 97 | equal(dom.firstChild.value, 'bar', 'Value of email input is two way data bound data -> element'); 98 | equalTokens(dom, '', 'Value Attribute on email input is two way data bound element -> data'); 99 | dom.firstChild.value = 'Hello World'; 100 | dom.firstChild.dispatchEvent(evt); 101 | equal(data.get('bar'), 'Hello World', 'Value on email input is two way data bound element -> data'); 102 | 103 | 104 | 105 | template = compiler.compile('', {name: 'test/partial'}); 106 | data = new Model({foo:'bar', bar:'foo', bool: false}); 107 | template.render(dom, data); 108 | data.set('bar', 'bar'); 109 | equal(dom.firstChild.value, 'bar', 'Value of password input is two way data bound data -> element'); 110 | equalTokens(dom, '', 'Value Attribute on password input is two way data bound element -> data'); 111 | dom.firstChild.value = 'Hello World'; 112 | dom.firstChild.dispatchEvent(evt); 113 | equal(data.get('bar'), 'Hello World', 'Value on password input is two way data bound element -> data'); 114 | 115 | 116 | 117 | template = compiler.compile('', {name: 'test/partial'}); 118 | data = new Model({foo:'bar', bar:'foo', bool: false}); 119 | template.render(dom, data); 120 | data.set('bar', 'bar'); 121 | equal(dom.firstChild.value, 'bar', 'Value of search input is two way data bound data -> element'); 122 | equalTokens(dom, '', 'Value Attribute on search input is two way data bound element -> data'); 123 | dom.firstChild.value = 'Hello World'; 124 | dom.firstChild.dispatchEvent(evt); 125 | equal(data.get('bar'), 'Hello World', 'Value on search input is two way data bound element -> data'); 126 | 127 | 128 | 129 | template = compiler.compile('', {name: 'test/partial'}); 130 | data = new Model({foo:'bar', bar:'foo', bool: false}); 131 | template.render(dom, data); 132 | data.set('bar', 'bar'); 133 | equal(dom.firstChild.value, 'bar', 'Value of url input is two way data bound data -> element'); 134 | equalTokens(dom, '', 'Value Attribute on url input is two way data bound element -> data'); 135 | dom.firstChild.value = 'Hello World'; 136 | dom.firstChild.dispatchEvent(evt); 137 | equal(data.get('bar'), 'Hello World', 'Value on url input is two way data bound element -> data'); 138 | 139 | 140 | 141 | template = compiler.compile('', {name: 'test/partial'}); 142 | data = new Model({foo:'bar', bar:'foo', bool: false}); 143 | template.render(dom, data); 144 | data.set('bar', 'bar'); 145 | equal(dom.firstChild.value, 'bar', 'Value of tel input is two way data bound data -> element'); 146 | equalTokens(dom, '', 'Value Attribute on tel input is two way data bound element -> data'); 147 | dom.firstChild.value = 'Hello World'; 148 | dom.firstChild.dispatchEvent(evt); 149 | equal(data.get('bar'), 'Hello World', 'Value on tel input is two way data bound element -> data'); 150 | 151 | template = compiler.compile('', {name: 'test/partial'}); 152 | data = new Model({num: 1}); 153 | template.render(dom, data); 154 | equal(dom.firstChild.value, '1', 'Value of number input is set on first render'); 155 | data.set('num', 2); 156 | equal(dom.firstChild.value, '2', 'Value of number input is data bound data -> element'); 157 | dom.firstChild.value = 10; 158 | dom.firstChild.dispatchEvent(evt); 159 | equal(data.get('num'), 10, 'Value on number input is two way data bound element -> data and converted to number type'); 160 | dom.firstChild.value = 'foobar'; 161 | dom.firstChild.dispatchEvent(evt); 162 | equal(data.get('num'), undefined, "When a number element's value is set to a non-numerical value, the data is set to undefined"); 163 | dom.firstChild.value = '1.'; 164 | dom.firstChild.dispatchEvent(evt); 165 | equal(data.get('num'), undefined, "Number element considers trailing decimal to be a non-numerical value and the data is set to undefined accordingly"); 166 | dom.firstChild.value = '1.5'; 167 | dom.firstChild.dispatchEvent(evt); 168 | data.set('num', 'foo'); 169 | equal(dom.firstChild.value, '', "When data is set to a non-numeric value, the number input's value is set to empty string"); 170 | 171 | 172 | template = compiler.compile('', {name: 'test/partial'}); 173 | data = new Model({bool: false}); 174 | template.render(dom, data); 175 | equalTokens(dom, "", 'Checked Attribute on checkbox not present on false'); 176 | data.set('bool', true); 177 | equalTokens(dom, "", 'Checked Attribute on checkbox present on true, and is data bound'); 178 | 179 | 180 | template = compiler.compile('
', {name: 'test/partial'}); 181 | data = new Model({foo: 'foo', bar: 'bar'}); 182 | template.render(dom, data); 183 | equalTokens(dom, "
", 'Multiple attribute morphs concat properly'); 184 | data.set('bar', 'foo'); 185 | equalTokens(dom, "
", 'Concatted attributes are data bound'); 186 | data.set('bar', undefined); 187 | equalTokens(dom, "
", 'Concatted attributes handle undefined values'); 188 | 189 | 190 | }); 191 | -------------------------------------------------------------------------------- /packages/rebound-data/test/rebound_model_test.js: -------------------------------------------------------------------------------- 1 | import { Model, Collection } from 'rebound-data/rebound-data'; 2 | 3 | // Notify all of a object's observers of the change, execute the callback 4 | function notify(obj, path) { 5 | // If path is not an array of keys, wrap it in array 6 | path = (_.isString(path)) ? [path] : path; 7 | 8 | // For each path, alert each observer and call its callback 9 | _.each(path, function(path){ 10 | if(obj.__observers && _.isArray(obj.__observers[path])){ 11 | _.each(obj.__observers[path], function(callback, index) { 12 | if(callback){ callback(); } 13 | else{ delete obj.__observers[path][index]; } 14 | }); 15 | } 16 | }); 17 | } 18 | 19 | QUnit.test('Rebound Data - Model', function() { 20 | var model, collection, model2, model3; 21 | 22 | // Shallow Set - Primitive Values 23 | model = new Model(); 24 | model.set('str', 'test'); 25 | model.set('int', 1); 26 | model.set('bool', false); 27 | deepEqual( model.attributes, {str: 'test', int: 1, bool: false}, 'Model.set works with primitive values at top level' ); 28 | 29 | // Shallow Set - Primitive Value Constructors 30 | model = new Model(); 31 | model.set('str', new String('test')); // jshint ignore:line 32 | model.set('int', new Number(1)); // jshint ignore:line 33 | model.set('bool', new Boolean(false)); // jshint ignore:line 34 | deepEqual( model.attributes, {str: 'test', int: 1, bool: false}, 'Model.set works with primitive values created by primitive contructors' ); 35 | 36 | 37 | 38 | // Shallow Toggle Boolean Values 39 | model.toggle('bool'); 40 | deepEqual( model.attributes, {str: 'test', int: 1, bool: true}, 'Model.toggle works with boolean values at top level' ); 41 | 42 | // Shallow Set - Complex Objects 43 | model = new Model(); 44 | model.set('obj', {a:1}); 45 | equal(model.attributes.obj.isModel, true, 'Model.set promotes vanilla objects to Models'); 46 | model.set('obj', {bool:false}); 47 | deepEqual(model.attributes.obj.attributes, {a:1, bool:false}, 'Model.set adds to existing models when passed vanilla objects'); 48 | 49 | // Deep Set - Primitive Values 50 | model.set('obj.a', 2); 51 | deepEqual(model.attributes.obj.attributes.a, 2, 'Model.set automatically creates extra models where needed'); 52 | 53 | // Deep Set - Complex Objects 54 | model.set('obj', {b: 3}); 55 | deepEqual(model.attributes.obj.attributes.b, 3, 'Model.set automatically creates extra models where needed'); 56 | deepEqual(model.attributes.obj.attributes.a, 2, 'Model.set does not destroy existing values'); 57 | 58 | // Deep Set - Auto Object Creation 59 | model.set('depth.test', 1); 60 | deepEqual(model.attributes.depth.attributes.test, 1, 'Model.set automatically creates extra models where needed'); 61 | 62 | // Deep Toggle 63 | model.toggle('obj.bool'); 64 | deepEqual(model.attributes.obj.attributes, {a:2, b:3, bool:true}, 'Model.toggle works with nested boolean values'); 65 | 66 | 67 | 68 | model = new Model(); 69 | model2 = new Model({b:2}); 70 | model.set('obj', model2); 71 | equal('c' + ((parseInt(model2.cid.replace('c', ''))) + 1), model.attributes.obj.cid, 'Model.set, when passed another model, clones that model.'); 72 | model3 = new Model({c:3}); 73 | var cid = model.attributes.obj.cid; 74 | model.set('obj', model3); 75 | equal(model.attributes.obj.cid, cid, 'Model.set, when passed another model, merges with the existing model.'); 76 | deepEqual(model.attributes.obj.attributes, {b: 2, c: 3}, 'Model.set, when passed another model, merges their attributes.'); 77 | 78 | 79 | 80 | model = new Model(); 81 | model.set('arr', [{a:1}]); 82 | equal(model.attributes.arr.isCollection, true, 'Model.set promotes vanilla arrays to Collections'); 83 | 84 | 85 | 86 | model = new Model(); 87 | model.set('obj', {obj2:{a:1}}); 88 | equal(model.attributes.obj.attributes.obj2.isModel, true, 'Model.set promotes nested vanilla objects to Models'); 89 | 90 | 91 | 92 | model = new Model(); 93 | model.set('test', 'foo'); 94 | equal(model.get('test'), 'foo', 'Model.get works 1 level deep'); 95 | 96 | 97 | 98 | model = new Model(); 99 | model.set('test', {'test2': {'test3': 'foo'}}); 100 | equal(model.get('test.test2.test3'), 'foo', 'Model.get works n levels deep - Models only'); 101 | 102 | model = new Model(); 103 | model.set('test', {'test2': [{'test3': 'foo'}]}); 104 | equal(model.get('test.test2[0].test3'), 'foo', 'Model.get works n levels deep - Collections included'); 105 | equal(model.get('test.test2').__path(), 'test.test2', 'Nested Models inherit path of parents'); 106 | equal(model.get('test.test2[0]').__path(), 'test.test2[0]', 'Nested Collections inherit path of parents'); 107 | deepEqual(model.toJSON(), {'test': {'test2': [{'test3': 'foo'}]}}, 'Model\'s toJSON method is recursive'); 108 | 109 | model.set('test.test2.[0].test3', model); 110 | deepEqual(model.toJSON(), {'test': {'test2': [{'test3': model.cid}]}}, 'Model\'s toJSON handles cyclic dependancies'); 111 | 112 | equal(model.get('test').__parent__.cid, model.cid, 'Model\'s ancestery is set when child of a Model'); 113 | deepEqual(model.get('test.test2[0]').__parent__, model.get('test.test2'), 'Model\'s ancestry is set when child of a Collection'); 114 | 115 | 116 | model.on('change', function(model, options){ 117 | deepEqual(model.changedAttributes(), {test3: 'foo'}, 'Events are propagated up to parent'); 118 | }); 119 | model.set('test.test2.[0].test3', 'foo'); 120 | 121 | 122 | collection = Collection.extend({ 123 | model: Model.extend({ 124 | defaults: { 125 | test: true 126 | } 127 | }) 128 | }); 129 | model = new Model({ 130 | prop: true, 131 | arr: (new collection()), 132 | obj: { foo: {bar: 'bar'} }, 133 | get func(){ 134 | return this.get('obj'); 135 | } 136 | }); 137 | model.defaults = { prop: true }; 138 | model.set('arr', [{foo: 'bar'}, {biz: 'baz'}, {test: false}]); 139 | deepEqual(model.toJSON(), {prop: true, 'arr': [{foo: 'bar', test: true}, {biz: 'baz', test: true}, {test: false}], obj: {foo: {bar: 'bar'}}, func: {foo: {bar: 'bar'}}}, 'Defaults set in a component are retained'); 140 | 141 | model.reset({prop: false, arr: [{id: 1}], obj: {foo: {test: true}}}); 142 | deepEqual(model.toJSON(), {prop: false, arr: [{id: 1, test: true}], obj: {foo: {test: true}}, func: {foo: {test: true}}}, 'Calling reset() with new values on a model resets it with these new values'); 143 | deepEqual(model.changed, {prop: false, arr: [{id: 1}], obj: {foo: {bar: undefined, test: true}}, func: {foo: {bar: undefined}}}, 'Calling reset() with new values on a model resets it with these new values and properly sets its changed property.'); 144 | 145 | model.reset(); 146 | deepEqual(model.toJSON(), {prop: true, arr: [], obj: {foo: {}}, func: {foo: {}}}, 'Calling reset() on a model resets all of its properties and children'); 147 | 148 | model.unset('prop'); 149 | model.reset(); 150 | deepEqual(model.get('prop'), true, 'Calling reset() on a model resets all unset values back to defaults'); 151 | 152 | 153 | model = new Model({foo: {bar: 1}}); 154 | model.set('foo.bar', {a:1}); 155 | deepEqual(model.toJSON(), {foo: {bar: {a: 1}}}, 'Setting a deep existing value to a complex object with .set results in the correct object.'); 156 | deepEqual(model.get('foo.bar').__parent__.cid, model.get('foo').cid, 'Setting a deep existing value to a complex object with .set results in the correct object.'); 157 | 158 | 159 | model = new Model(); 160 | model.set('a.b.c', {d: 1}); 161 | deepEqual(model.toJSON(), {a: {b: {c: {d: 1}}}}, 'Calling set on a deep object that does not exist creates it.'); 162 | equal(model.get('a.b.c').__parent__.cid, model.get('a.b').cid, 'Deep Models\' ancestery is set when automatically generating objects.'); 163 | equal(model.get('a.b').__parent__.cid, model.get('a').cid, 'Deep Models\' ancestery is set when automatically generating objects.'); 164 | equal(model.get('a').__parent__.cid, model.cid, 'Deep Models\' ancestery is set when automatically generating objects.'); 165 | 166 | 167 | model = new Model({ 168 | a: { 169 | b: { 170 | c: { 171 | d: 0 172 | } 173 | } 174 | } 175 | }); 176 | 177 | model.get('a.b.c').on('change:d', function(model, value, options){ 178 | equal(model.get('d'), value, 'Change events propagated with proper name on the object that changed'); 179 | }); 180 | model.get('a.b').on('change:c.d', function(model, value, options){ 181 | equal(model.get('d'), value, 'Change events propagated with proper name 1 layer up'); 182 | }); 183 | model.get('a').on('change:b.c.d', function(model, value, options){ 184 | equal(model.get('d'), value, 'Change events propagated with proper name 2 layers up'); 185 | }); 186 | model.on('change:a.b.c.d', function(model, value, options){ 187 | equal(model.get('d'), value, 'Change events propagated with proper name on root'); 188 | }); 189 | model.set('a.b.c.d', 1); 190 | 191 | 192 | var NewModel = Model.extend({ 193 | defaults: { 194 | val: 'foo', 195 | get prop(){ 196 | return this.get('val'); 197 | } 198 | } 199 | }); 200 | 201 | model = new NewModel(); 202 | 203 | equal(model.get('prop'), model.get('val'), 'Extended Rebound models with a computed property in its defaults hash are parsed succesfully.'); 204 | 205 | 206 | 207 | 208 | }); 209 | 210 | // When set is called with option: {defaults: true}, it sets the defaults object to the property passed. 211 | --------------------------------------------------------------------------------