4 |
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 |
3 |
Index Page 6!
4 | Index 1 Index
5 | Index 1 Test
6 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/test/dummy-apps/6/service1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
', '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('
', {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('
`);
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 |
153 |
154 | {{> far/boo}}
155 |
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 |
186 |
187 |
{{foo}}
188 |
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('