Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow asynchronous load of templates in components #714

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/docs/guide/_sections/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
148 changes: 84 additions & 64 deletions spec/rivets/component_binding.js
Original file line number Diff line number Diff line change
@@ -1,79 +1,99 @@
describe('Component binding', function() {
var scope, element, component, componentRoot

beforeEach(function() {
element = document.createElement('div')
element.innerHTML = '<test></test>'
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 '<h1>test</h1>'}
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 = '<test></test>'
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 '<h1>test</h1>'
}
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('<h1>{ name }</h1>')
componentRoot.innerHTML.should.equal('<h1>' + scope.name + '</h1>')
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('<h1>{ name }</h1>')
rivets.bind(element)
describe('when "template" is a function', function () {
it('renders returned string as component template', function () {
component.template = sinon.stub().returns('<h1>{ name }</h1>')
rivets.bind(element)

componentRoot.innerHTML.should.equal('<h1>' + scope.name + '</h1>')
componentRoot.innerHTML.should.equal('<h1>' + scope.name + '</h1>')
})
})
})

})
43 changes: 26 additions & 17 deletions src/bindings.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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: =>
Expand Down