Merge pull request #48 from mikeric/iteration-binding
Iteration binding
Showing
4 changed files
with
146 additions
and
8 deletions
... | @@ -104,6 +104,7 @@ Available bindings out-of-the-box: | ... | @@ -104,6 +104,7 @@ Available bindings out-of-the-box: |
104 | - data-unchecked | 104 | - data-unchecked |
105 | - data-*[attribute]* | 105 | - data-*[attribute]* |
106 | - data-on-*[event]* | 106 | - data-on-*[event]* |
107 | - data-each-*[item]* | ||
107 | 108 | ||
108 | #### Formatters | 109 | #### Formatters |
109 | 110 | ||
... | @@ -143,9 +144,34 @@ Computed properties are functions that get re-evaluated when one or more depende | ... | @@ -143,9 +144,34 @@ Computed properties are functions that get re-evaluated when one or more depende |
143 | 144 | ||
144 | #### Iteration Binding | 145 | #### Iteration Binding |
145 | 146 | ||
146 | Even though a binding routine for `each-item` will likely be included in Rivets.js, you can use the `data-html` binding along with a set of formatters in the interim to do sorting and iterative rendering of collections (amongst other cool things). | 147 | Use the `data-each-[item]` binding to have Rivets.js automatically loop over items in an array and append bound instances of that element. Within that element you can bind to the iterated item as well as any contexts that are available in the parent view. |
147 | 148 | ||
148 | <ul data-html="model.tags | sort | tagList"></ul> | 149 | ``` |
150 | <ul> | ||
151 | <li data-each-todo="list.todos"> | ||
152 | <input type="checkbox" data-checked="todo.done"> | ||
153 | <span data-text="todo.summary"></span> | ||
154 | </li> | ||
155 | <ul> | ||
156 | ``` | ||
157 | |||
158 | If the array you're binding to contains non-model objects (they don't conform to your adapter), you can still iterate over them, just make sure to use the adapter bypass syntax — in doing so, the iteration binding will still update when the array changes, however the individual items will not since they'd be bypassing the `adapter.subscribe`. | ||
159 | |||
160 | ``` | ||
161 | <ul> | ||
162 | <li data-each-link="item.links"> | ||
163 | <a data-href="link:url" data-text="link:title"></span> | ||
164 | </li> | ||
165 | </ul> | ||
166 | ``` | ||
167 | |||
168 | Also note that you may bind to the iterated item directly on the parent element. | ||
169 | |||
170 | ``` | ||
171 | <ul> | ||
172 | <li data-each-tag="item.tags" data-text="tag:name"></li> | ||
173 | </ul> | ||
174 | ``` | ||
149 | 175 | ||
150 | ## Building and Testing | 176 | ## Building and Testing |
151 | 177 | ... | ... |
... | @@ -116,6 +116,19 @@ describe('Rivets.Binding', function() { | ... | @@ -116,6 +116,19 @@ describe('Rivets.Binding', function() { |
116 | expect(binding.routine).toHaveBeenCalledWith(el, funcb, funca); | 116 | expect(binding.routine).toHaveBeenCalledWith(el, funcb, funca); |
117 | }); | 117 | }); |
118 | }); | 118 | }); |
119 | |||
120 | describe('on an iteration binding', function(){ | ||
121 | beforeEach(function(){ | ||
122 | binding.options.special = 'iteration'; | ||
123 | }); | ||
124 | |||
125 | it('performs the binding routine with the supplied collection and binding', function() { | ||
126 | spyOn(binding, 'routine'); | ||
127 | array = [{name: 'a'}, {name: 'b'}]; | ||
128 | binding.set(array); | ||
129 | expect(binding.routine).toHaveBeenCalledWith(el, array, binding); | ||
130 | }); | ||
131 | }); | ||
119 | }); | 132 | }); |
120 | 133 | ||
121 | describe('publish()', function() { | 134 | describe('publish()', function() { | ... | ... |
... | @@ -2,7 +2,7 @@ describe('Functional', function() { | ... | @@ -2,7 +2,7 @@ describe('Functional', function() { |
2 | var data, bindData, el, input; | 2 | var data, bindData, el, input; |
3 | 3 | ||
4 | beforeEach(function() { | 4 | beforeEach(function() { |
5 | data = new Data({foo: 'bar'}); | 5 | data = new Data({foo: 'bar', items: [{name: 'a'}, {name: 'b'}]}); |
6 | bindData = {data: data}; | 6 | bindData = {data: data}; |
7 | el = document.createElement('div'); | 7 | el = document.createElement('div'); |
8 | input = document.createElement('input'); | 8 | input = document.createElement('input'); |
... | @@ -100,6 +100,52 @@ describe('Functional', function() { | ... | @@ -100,6 +100,52 @@ describe('Functional', function() { |
100 | expect(input.value).toBe(data.get('foo')); | 100 | expect(input.value).toBe(data.get('foo')); |
101 | }); | 101 | }); |
102 | }); | 102 | }); |
103 | |||
104 | describe('Iteration', function() { | ||
105 | beforeEach(function(){ | ||
106 | list = document.createElement('ul'); | ||
107 | el.appendChild(list); | ||
108 | listItem = document.createElement('li'); | ||
109 | listItem.setAttribute('data-each-item', 'data.items'); | ||
110 | list.appendChild(listItem); | ||
111 | }); | ||
112 | |||
113 | it('should loop over a collection and create new instances of that element + children', function() { | ||
114 | expect(el.getElementsByTagName('li').length).toBe(1); | ||
115 | rivets.bind(el, bindData); | ||
116 | expect(el.getElementsByTagName('li').length).toBe(2); | ||
117 | }); | ||
118 | |||
119 | it('should re-loop over the collection and create new instances when the array changes', function() { | ||
120 | rivets.bind(el, bindData); | ||
121 | expect(el.getElementsByTagName('li').length).toBe(2); | ||
122 | |||
123 | newItems = [{name: 'a'}, {name: 'b'}, {name: 'c'}]; | ||
124 | data.set({items: newItems}); | ||
125 | expect(el.getElementsByTagName('li').length).toBe(3); | ||
126 | }); | ||
127 | |||
128 | it('should allow binding to the iterated item as well as any parent contexts', function() { | ||
129 | span1 = document.createElement('span'); | ||
130 | span1.setAttribute('data-text', 'item:name') | ||
131 | span2 = document.createElement('span'); | ||
132 | span2.setAttribute('data-text', 'data.foo') | ||
133 | listItem.appendChild(span1); | ||
134 | listItem.appendChild(span2); | ||
135 | |||
136 | rivets.bind(el, bindData); | ||
137 | expect(el.getElementsByTagName('span')[0]).toHaveTheTextContent('a'); | ||
138 | expect(el.getElementsByTagName('span')[1]).toHaveTheTextContent('bar'); | ||
139 | }); | ||
140 | |||
141 | it('should allow binding to the iterated element directly', function() { | ||
142 | listItem.setAttribute('data-text', 'item:name'); | ||
143 | listItem.setAttribute('data-class', 'data.foo'); | ||
144 | rivets.bind(el, bindData); | ||
145 | expect(el.getElementsByTagName('li')[0]).toHaveTheTextContent('a'); | ||
146 | expect(el.getElementsByTagName('li')[0].className).toBe('bar'); | ||
147 | }); | ||
148 | }); | ||
103 | }); | 149 | }); |
104 | 150 | ||
105 | describe('Updates', function() { | 151 | describe('Updates', function() { | ... | ... |
... | @@ -15,10 +15,10 @@ class Rivets.Binding | ... | @@ -15,10 +15,10 @@ class Rivets.Binding |
15 | # element, the type of binding, the model object and the keypath at which | 15 | # element, the type of binding, the model object and the keypath at which |
16 | # to listen for changes. | 16 | # to listen for changes. |
17 | constructor: (@el, @type, @model, @keypath, @options = {}) -> | 17 | constructor: (@el, @type, @model, @keypath, @options = {}) -> |
18 | if @options.special is "event" | 18 | @routine = switch @options.special |
19 | @routine = eventBinding @type | 19 | when "event" then eventBinding @type |
20 | else | 20 | when "iteration" then iterationBinding @type |
21 | @routine = Rivets.routines[@type] || attributeBinding @type | 21 | else Rivets.routines[@type] || attributeBinding @type |
22 | 22 | ||
23 | @formatters = @options.formatters || [] | 23 | @formatters = @options.formatters || [] |
24 | 24 | ||
... | @@ -47,6 +47,8 @@ class Rivets.Binding | ... | @@ -47,6 +47,8 @@ class Rivets.Binding |
47 | if @options.special is "event" | 47 | if @options.special is "event" |
48 | @routine @el, value, @currentListener | 48 | @routine @el, value, @currentListener |
49 | @currentListener = value | 49 | @currentListener = value |
50 | else if @options.special is "iteration" | ||
51 | @routine @el, value, @ | ||
50 | else | 52 | else |
51 | value = value.call(@model) if value instanceof Function | 53 | value = value.call(@model) if value instanceof Function |
52 | @routine @el, value | 54 | @routine @el, value |
... | @@ -85,6 +87,7 @@ class Rivets.Binding | ... | @@ -85,6 +87,7 @@ class Rivets.Binding |
85 | 87 | ||
86 | # Unsubscribes from the model and the element. | 88 | # Unsubscribes from the model and the element. |
87 | unbind: => | 89 | unbind: => |
90 | unless @options.bypass | ||
88 | Rivets.config.adapter.unsubscribe @model, @keypath, @set | 91 | Rivets.config.adapter.unsubscribe @model, @keypath, @set |
89 | 92 | ||
90 | if @options.dependencies?.length | 93 | if @options.dependencies?.length |
... | @@ -111,12 +114,25 @@ class Rivets.View | ... | @@ -111,12 +114,25 @@ class Rivets.View |
111 | # Builds the Rivets.Binding instances for the view. | 114 | # Builds the Rivets.Binding instances for the view. |
112 | build: => | 115 | build: => |
113 | @bindings = [] | 116 | @bindings = [] |
117 | skipNodes = [] | ||
118 | iterator = null | ||
114 | bindingRegExp = @bindingRegExp() | 119 | bindingRegExp = @bindingRegExp() |
115 | eventRegExp = /^on-/ | 120 | eventRegExp = /^on-/ |
121 | iterationRegExp = /^each-/ | ||
116 | 122 | ||
117 | parseNode = (node) => | 123 | parseNode = (node) => |
124 | unless node in skipNodes | ||
118 | for attribute in node.attributes | 125 | for attribute in node.attributes |
119 | if bindingRegExp.test attribute.name | 126 | if bindingRegExp.test attribute.name |
127 | type = attribute.name.replace bindingRegExp, '' | ||
128 | |||
129 | if iterationRegExp.test type | ||
130 | unless @models[type.replace iterationRegExp, ''] | ||
131 | skipNodes.push n for n in node.getElementsByTagName '*' | ||
132 | iterator = [attribute] | ||
133 | |||
134 | for attribute in iterator or node.attributes | ||
135 | if bindingRegExp.test attribute.name | ||
120 | options = {} | 136 | options = {} |
121 | 137 | ||
122 | type = attribute.name.replace bindingRegExp, '' | 138 | type = attribute.name.replace bindingRegExp, '' |
... | @@ -129,6 +145,7 @@ class Rivets.View | ... | @@ -129,6 +145,7 @@ class Rivets.View |
129 | options.bypass = path.indexOf(":") != -1 | 145 | options.bypass = path.indexOf(":") != -1 |
130 | keypath = splitPath.join() | 146 | keypath = splitPath.join() |
131 | 147 | ||
148 | if model | ||
132 | if dependencies = context.shift() | 149 | if dependencies = context.shift() |
133 | options.dependencies = dependencies.split /\s+/ | 150 | options.dependencies = dependencies.split /\s+/ |
134 | 151 | ||
... | @@ -136,7 +153,18 @@ class Rivets.View | ... | @@ -136,7 +153,18 @@ class Rivets.View |
136 | type = type.replace eventRegExp, '' | 153 | type = type.replace eventRegExp, '' |
137 | options.special = "event" | 154 | options.special = "event" |
138 | 155 | ||
139 | @bindings.push new Rivets.Binding node, type, model, keypath, options | 156 | if iterationRegExp.test type |
157 | type = type.replace iterationRegExp, '' | ||
158 | options.special = "iteration" | ||
159 | |||
160 | binding = new Rivets.Binding node, type, model, keypath, options | ||
161 | binding.view = @ | ||
162 | |||
163 | @bindings.push binding | ||
164 | |||
165 | if iterator | ||
166 | node.removeAttribute(a.name) for a in iterator | ||
167 | iterator = null | ||
140 | 168 | ||
141 | for el in @els | 169 | for el in @els |
142 | parseNode el | 170 | parseNode el |
... | @@ -180,6 +208,31 @@ eventBinding = (event) -> (el, bind, unbind) -> | ... | @@ -180,6 +208,31 @@ eventBinding = (event) -> (el, bind, unbind) -> |
180 | bindEvent el, event, bind if bind | 208 | bindEvent el, event, bind if bind |
181 | unbindEvent el, event, unbind if unbind | 209 | unbindEvent el, event, unbind if unbind |
182 | 210 | ||
211 | # Returns an iteration binding routine for the specified collection. | ||
212 | iterationBinding = (name) -> (el, collection, binding) -> | ||
213 | if binding.iterated? | ||
214 | for iteration in binding.iterated | ||
215 | iteration.view.unbind() | ||
216 | iteration.el.parentNode.removeChild iteration.el | ||
217 | else | ||
218 | binding.marker = document.createComment " rivets: each-#{name} " | ||
219 | el.parentNode.insertBefore binding.marker, el | ||
220 | el.parentNode.removeChild el | ||
221 | |||
222 | binding.iterated = [] | ||
223 | |||
224 | for item in collection | ||
225 | data = {} | ||
226 | data[n] = m for n, m of binding.view.models | ||
227 | data[name] = item | ||
228 | itemEl = el.cloneNode true | ||
229 | previous = binding.iterated[binding.iterated.length - 1] or binding.marker | ||
230 | binding.marker.parentNode.insertBefore itemEl, previous.nextSibling | ||
231 | |||
232 | binding.iterated.push | ||
233 | el: itemEl | ||
234 | view: rivets.bind itemEl, data | ||
235 | |||
183 | # Returns an attribute binding routine for the specified attribute. This is what | 236 | # Returns an attribute binding routine for the specified attribute. This is what |
184 | # is used when there are no matching routines for an identifier. | 237 | # is used when there are no matching routines for an identifier. |
185 | attributeBinding = (attr) -> (el, value) -> | 238 | attributeBinding = (attr) -> (el, value) -> | ... | ... |
-
Please register or sign in to post a comment