diff --git a/lib/each.js b/lib/each.js index 6a70232..c50b271 100644 --- a/lib/each.js +++ b/lib/each.js @@ -2,10 +2,6 @@ module.exports = function(el, val) { var self = this; - // get the reactive constructor from the current reactive instance - // TODO(shtylman) port over adapter and bindings from instance? - var Reactive = self.reactive.constructor; - var val = val.split(/ +/); el.removeAttribute('each'); @@ -31,11 +27,7 @@ module.exports = function(el, val) { var views = []; function childView(el, model) { - return Reactive(el, model, { - delegate: self.view, - adapter: self.reactive.opt.adapter, - bindings: self.reactive.bindings - }); + return self.reactive.subview(model).render(el) } var array; diff --git a/lib/index.js b/lib/index.js index b7f1f00..61b027d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,43 +30,49 @@ exports = module.exports = Reactive; * @api public */ -function Reactive(el, model, opt) { - if (!(this instanceof Reactive)) return new Reactive(el, model, opt); +function Reactive(model, opt) { + if (!(this instanceof Reactive)) return new Reactive(model, opt); opt = opt || {}; - if (typeof el === 'string') { - el = domify(el); - } - var self = this; self.opt = opt || {}; self.model = model || {}; self.adapter = (opt.adapter || Adapter)(self.model); - self.el = el; self.view = opt.delegate || Object.create(null); - self.bindings = opt.bindings || Object.create(null); - // TODO undo this crap and just export bindings regularly - // not that binding order matters!! - bindings({ - bind: function(name, fn) { - self.bindings[name] = fn; - } - }); - - self._bind(this.el, []); + this.use(bindings) } Emitter(Reactive.prototype); +/** + * Render the view. + * + * @param {String|DOM} el + * @return {Reactive} + * @api public + */ + +Reactive.prototype.render = function (el) { + var self = this; + + if (typeof el === 'string') { + el = domify(el); + } + + self.el = el; + self._bind(this.el, []); + return self; +} + /** * Subscribe to changes on `prop`. * * @param {String} prop * @param {Function} fn * @return {Reactive} - * @api private + * @api public */ Reactive.prototype.sub = function(prop, fn){ @@ -288,24 +294,18 @@ Reactive.prototype._bind = function() { Reactive.prototype.bind = function(name, fn) { var self = this; if ('object' == typeof name) { - for (var key in name) { - this.bind(key, name[key]); - } - return; + Object.keys(name).forEach(function (key) { + self.bind(key, name[key]); + }) + return self; } - var els = query.all('[' + name + ']', this.el); - if (this.el.hasAttribute && this.el.hasAttribute(name)) { - els = [].slice.call(els); - els.unshift(this.el); + if (!self.el) { + self.bindings[name] = fn; + return self; } - if (!els.length) return; - debug('bind [%s] (%d elements)', name, els.length); - for (var i = 0; i < els.length; i++) { - var binding = new Binding(name, this, els[i], fn); - binding.bind(); - } + throw new Error('.bind() cannot be called after .render()') }; /** @@ -326,6 +326,7 @@ Reactive.prototype.destroy = function() { self.adapter.unsubscribeAll(); self.emit('destroyed'); self.removeAllListeners(); + delete self.el; }; /** @@ -339,6 +340,22 @@ Reactive.prototype.use = function(fn) { return this; }; +/** + * Create a Dommit view with the given model but the same bindings, etc. + * + * @param {Object} model + * @return {Reactive} subview + * @api public + */ + +Reactive.prototype.subview = function (model) { + return new Reactive(model, { + bindings: this.bindings, + adapter: this.opt.adapter, + bindings: this.bindings, + delegate: this.view || {} + }) +} function inDomTree(el) { try { diff --git a/test/adapters.js b/test/adapters.js index 3f81725..8522a23 100644 --- a/test/adapters.js +++ b/test/adapters.js @@ -73,17 +73,17 @@ describe('custom adapter', function() { }); it('setting obj[prop] should update view', function() { - reactive(el, person, { + reactive(person, { adapter: BackboneAdapter - }); + }).render(el); person.set('name', 'TJ'); assert('TJ' == el.children[0].textContent); }); it('should not double set when updating reactive instance', function(done) { - var react = reactive(el, person, { + var react = reactive(person, { adapter: BackboneAdapter - }); + }).render(el); react.sub('name', function(val) { assert.equal(val, 'TJ'); done(); @@ -92,18 +92,18 @@ describe('custom adapter', function() { }); it('shouldnt update view after being unsubscribed', function() { - var react = reactive(el, person, { + var react = reactive(person, { adapter: BackboneAdapter - }); + }).render(el); react.unsub('name'); person.set('name', 'TJ'); assert('Matt' == el.children[0].textContent); }); it('setting view should update object', function() { - var react = reactive(el, person, { + var react = reactive(person, { adapter: BackboneAdapter - }); + }).render(el); react.set('name', 'TJ'); assert('TJ' == el.children[0].textContent); assert('TJ' == person.get('name')); diff --git a/test/attr-interpolation.js b/test/attr-interpolation.js index 9f5da41..77b3974 100644 --- a/test/attr-interpolation.js +++ b/test/attr-interpolation.js @@ -9,21 +9,21 @@ describe('attr interpolation', function(){ it('should support initialization', function(){ var el = domify(''); var user = { id: '1234' }; - var view = reactive(el, user); + var view = reactive(user).render(el); assert('/download/1234' == el.getAttribute('href')); }) it('should ignore whitespace', function(){ var el = domify(''); var user = { id: '1234' }; - var view = reactive(el, user); + var view = reactive(user).render(el); assert('/download/1234' == el.getAttribute('href')); }) it('should react to changes', function(){ var el = domify(''); var user = { id: '1234' }; - var view = reactive(el, user); + var view = reactive(user).render(el); assert('/download/1234' == el.getAttribute('href')); @@ -34,7 +34,7 @@ describe('attr interpolation', function(){ it('should support multiple attributes', function(){ var el = domify('Download {{file}}'); var user = { id: '1234' }; - var view = reactive(el, user); + var view = reactive(user).render(el); assert('/download/1234' == el.getAttribute('href')); assert('file-1234' == el.getAttribute('id')); @@ -47,7 +47,7 @@ describe('attr interpolation', function(){ it('should support multiple properties', function(){ var el = domify(''); var user = { id: '1234', file: 'something' }; - var view = reactive(el, user); + var view = reactive(user).render(el); assert('/download/1234-something' == el.getAttribute('href')); diff --git a/test/bindings.js b/test/bindings.js index 716f441..f674ffa 100644 --- a/test/bindings.js +++ b/test/bindings.js @@ -8,14 +8,12 @@ var reactive = require('../'); describe('reactive.bind(name, fn)', function(){ it('should define a new binding', function(done){ var el = domify('
hey
') + + assert(view.el.textContent == 'hey!') + + function addExclamation(el, attr) { + el.textContent = el.textContent + '!' + } + }) + + it('should allow plugins to add bindings', function () { + var view = reactive() + .use(addExclamationPlugin) + .render('hey
') + + assert(view.el.textContent == 'hey!') + + function addExclamationPlugin(reactive) { + reactive.bind('add-exclamation', function (el, attr) { + el.textContent = el.textContent + '!' + }) + } + }) + + it('should allow plugins to get/set values', function () { + var view = reactive({ foo: 'bar' }) + .use(bindFoo) + .render('') + + assert(view.el.textContent == 'GO!') + assert(view.get('foo') == 'not-bar') + + function bindFoo(reactive) { + reactive.bind(reactive.get('foo'), function (el) { + el.textContent = 'GO!' + reactive.set('foo', 'not-bar') + }) + } + }) + + it('should pass bindings to subviews', function () { + var view = reactive() + .bind('lolz', function (el, attr) { el.textContent = 'lol' }) + .render('') + + var subview = view.subview() + .render('') + + assert(subview.el.textContent == 'lol') + }) + + it('should pass bindings set in plugins to subviews', function () { + var view = reactive() + .use(function (reactive) { reactive.bind('lolz', function (el, attr) { el.textContent = 'lol' }) }) + .render('') + + var subview = view.subview() + .render('') + + assert(subview.el.textContent == 'lol') + }) + + it('should pass bindings set in plugins to each-created subviews', function () { + var view = reactive({arr: [1,2,3]}) + .use(function (reactive) { reactive.bind('lolz', function (el, attr) { el.textContent = 'lol' }) }) + .render('Has a file
Has a file
Has a file
{{first}} {{last}} is a {{species}}
{{first + " " + last}}
'); var pet = { first: 'tobi', last: 'holowaychuk' }; - var view = reactive(el, pet); + var view = reactive(pet).render(el); assert('tobi holowaychuk' == el.textContent); view.set('last', 'ferret'); @@ -65,7 +65,7 @@ describe('text interpolation', function(){ } }; - reactive(el, pet); + reactive(pet).render(el); assert('first: Loki' == el.textContent); }) @@ -80,7 +80,7 @@ describe('text interpolation', function(){ ] }; - var view = reactive(el, pet); + var view = reactive(pet).render(el); assert('first: Loki, last: Jane' == el.textContent); view.set('siblings', ['Loki', 'Abby']); @@ -96,7 +96,7 @@ describe('text interpolation', function(){ last: function(){ return 'the Pet' } }; - reactive(el, pet); + reactive(pet).render(el); assert('name: Loki the Pet' == el.textContent); }) @@ -113,9 +113,9 @@ describe('text interpolation', function(){ casual: function(){ return pet.first() + ' ' + pet.last() } } - reactive(el, pet, { + reactive(pet, { delegate: view - }); + }).render(el); assert.equal('name: Loki the Pet', el.textContent); }) @@ -123,23 +123,23 @@ describe('text interpolation', function(){ it('should support the root element', function(){ var el = domify('Hello {{name}}'); var user = { name: 'Tobi' }; - reactive(el, user); + reactive(user).render(el); assert('Hello Tobi' == el.textContent); }) it('should support calling a property as a function', function(){ var model = { name: { first: 'Some Really Long Name' } }; - var view = reactive('
Hello {{name.first.slice(0,6)}}
', model); + var view = reactive(model).render('Hello {{name.first.slice(0,6)}}
'); assert('Hello Some R' == view.el.textContent); var model = { name: 'Some Really Long Name' }; - var view = reactive('Hello {{name.slice(0,4)}}
', model); + var view = reactive(model).render('Hello {{name.slice(0,4)}}
'); assert('Hello Some' == view.el.textContent); }) it('should support setting base property', function(){ var model = { name: { first: 'foobar' } }; - var view = reactive('{{name.first}}
', model); + var view = reactive(model).render('{{name.first}}
'); assert('foobar' == view.el.textContent); view.set('name', { first: 'baz' });