From b8770c4de648aa7db219c2c3a5ef604096a13688 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Cazeaux Date: Wed, 26 Jul 2017 10:20:54 +0200 Subject: [PATCH] Allow asynchronous load of templates in components --- docs/docs/guide/_sections/components.md | 12 ++ spec/rivets/component_binding.js | 148 ++++++++++++++---------- src/bindings.coffee | 43 ++++--- 3 files changed, 122 insertions(+), 81 deletions(-) diff --git a/docs/docs/guide/_sections/components.md b/docs/docs/guide/_sections/components.md index 5e6a13323..4d6b70879 100644 --- a/docs/docs/guide/_sections/components.md +++ b/docs/docs/guide/_sections/components.md @@ -20,6 +20,18 @@ rivets.components['todo-item'] = { } ``` +To load a template asynchronously, use a callback in `template` function +```javascript +rivets.components['todo-item'] = { + // Return the template for the component. + template: function(done) { + setTimeout(function() { + done(JST['todos/todo-item']); + }, 100); + } +} +``` + To use the component inside of a template, simply use an element with the same tag name as the component's key. All attributes on the element will get evaluated as keypaths before being passed into the component's `initialize` function. ```html diff --git a/spec/rivets/component_binding.js b/spec/rivets/component_binding.js index 182197be6..075717e6b 100644 --- a/spec/rivets/component_binding.js +++ b/spec/rivets/component_binding.js @@ -1,79 +1,99 @@ -describe('Component binding', function() { - var scope, element, component, componentRoot - - beforeEach(function() { - element = document.createElement('div') - element.innerHTML = '' - componentRoot = element.firstChild - scope = { name: 'Rivets' } - component = rivets.components.test = { - initialize: sinon.stub().returns(scope), - template: function() { return '' } - } - }) - - it('renders "template" as a string', function() { - component.template = function() {return '

test

'} - rivets.bind(element) - - componentRoot.innerHTML.should.equal(component.template()) - }) - - it('binds binders on component root element only once', function() { - rivets.binders['test-binder'] = sinon.spy(); - componentRoot.setAttribute('rv-test-binder', 'true'); - rivets.bind(element); - - rivets.binders['test-binder'].calledOnce.should.be.true; - - delete rivets.binders['test-binder']; - }) - - describe('initialize()', function() { - var locals - - beforeEach(function() { - locals = { object: { name: 'Rivets locals' } } - componentRoot.setAttribute('item', 'object') +describe('Component binding', function () { + var scope, element, component, componentRoot + + beforeEach(function () { + element = document.createElement('div') + element.innerHTML = '' + componentRoot = element.firstChild + scope = {name: 'Rivets'} + component = rivets.components.test = { + initialize: sinon.stub().returns(scope), + template: function () { + return '' + } + } }) - it('receives element as first argument and attributes as second', function() { - rivets.bind(element, locals) + it('renders "template" as a string', function () { + component.template = function () { + return '

test

' + } + rivets.bind(element) - component.initialize.calledWith(componentRoot, { item: locals.object }).should.be.true + componentRoot.innerHTML.should.equal(component.template()) }) - it('receives primitives attributes', function() { - componentRoot.setAttribute('primitivestring', "'value'") - componentRoot.setAttribute('primitivenumber', "42") - componentRoot.setAttribute('primitiveboolean', "true") - rivets.bind(element, locals) - - component.initialize.calledWith(componentRoot, { item: locals.object, - primitivestring: 'value', - primitivenumber: 42, - primitiveboolean: true }) - .should.be.true + it('binds binders on component root element only once', function () { + rivets.binders['test-binder'] = sinon.spy(); + componentRoot.setAttribute('rv-test-binder', 'true'); + rivets.bind(element); + + rivets.binders['test-binder'].calledOnce.should.be.true; + + delete rivets.binders['test-binder']; }) - it('returns attributes assigned to "static" property as they are', function() { - var type = 'text' + describe('initialize()', function () { + var locals + + beforeEach(function () { + locals = {object: {name: 'Rivets locals'}} + componentRoot.setAttribute('item', 'object') + }) + + it('receives element as first argument and attributes as second', function () { + rivets.bind(element, locals) + + component.initialize.calledWith(componentRoot, {item: locals.object}).should.be.true + }) - component.static = ['type'] - componentRoot.setAttribute('type', type) - rivets.bind(element, locals) + it('receives primitives attributes', function () { + componentRoot.setAttribute('primitivestring', "'value'") + componentRoot.setAttribute('primitivenumber', "42") + componentRoot.setAttribute('primitiveboolean', "true") + rivets.bind(element, locals) + + component.initialize.calledWith(componentRoot, { + item: locals.object, + primitivestring: 'value', + primitivenumber: 42, + primitiveboolean: true + }) + .should.be.true + }) + + it('returns attributes assigned to "static" property as they are', function () { + var type = 'text' + + component.static = ['type'] + componentRoot.setAttribute('type', type) + rivets.bind(element, locals) + + component.initialize.calledWith(componentRoot, {item: locals.object, type: type}).should.be.true + }) + }) - component.initialize.calledWith(componentRoot, { item: locals.object, type: type }).should.be.true + describe('when "templateAsync" is defined', function () { + it('renders returned string as component template', function (done) { + component.template = function (sendTemplate) { + setTimeout(function () { + sendTemplate('

{ name }

') + componentRoot.innerHTML.should.equal('

' + scope.name + '

') + done(); + }, + 1) + } + rivets.bind(element) + }) }) - }) - describe('when "template" is a function', function() { - it('renders returned string as component template', function() { - component.template = sinon.stub().returns('

{ name }

') - rivets.bind(element) + describe('when "template" is a function', function () { + it('renders returned string as component template', function () { + component.template = sinon.stub().returns('

{ name }

') + rivets.bind(element) - componentRoot.innerHTML.should.equal('

' + scope.name + '

') + componentRoot.innerHTML.should.equal('

' + scope.name + '

') + }) }) - }) }) diff --git a/src/bindings.coffee b/src/bindings.coffee index e3f34947d..ee82bfb41 100644 --- a/src/bindings.coffee +++ b/src/bindings.coffee @@ -239,28 +239,37 @@ class Rivets.ComponentBinding extends Rivets.Binding if @componentView? @componentView.bind() else - @el.innerHTML = @component.template.call this - scope = @component.initialize.call @, @el, @locals() - @el._bound = true + if @component.template.length == 0 + @execBind.call this, @component.template.call this + else if @component.template.length == 1 + @component.template.call this, (template) => + @execBind.call this, template - options = {} + return - for option in Rivets.extensions - options[option] = {} - options[option][k] = v for k, v of @component[option] if @component[option] - options[option][k] ?= v for k, v of @view[option] + # execbinding with template + execBind: (template) => + @el.innerHTML = template + scope = @component.initialize.call @, @el, @locals() + @el._bound = true - for option in Rivets.options - options[option] = @component[option] ? @view[option] + options = {} - @componentView = new Rivets.View(Array.prototype.slice.call(@el.childNodes), scope, options) - @componentView.bind() + for option in Rivets.extensions + options[option] = {} + options[option][k] = v for k, v of @component[option] if @component[option] + options[option][k] ?= v for k, v of @view[option] - for key, observer of @observers - @upstreamObservers[key] = @observe @componentView.models, key, ((key, observer) => => - observer.setValue @componentView.models[key] - ).call(@, key, observer) - return + for option in Rivets.options + options[option] = @component[option] ? @view[option] + + @componentView = new Rivets.View(Array.prototype.slice.call(@el.childNodes), scope, options) + @componentView.bind() + + for key, observer of @observers + @upstreamObservers[key] = @observe @componentView.models, key, ((key, observer) => => + observer.setValue @componentView.models[key] + ).call(@, key, observer) # Intercept `Rivets.Binding::unbind` to be called on `@componentView`. unbind: =>