58598d14 by Michael Richards

Merge pull request #48 from mikeric/iteration-binding

Iteration binding
2 parents fb1984a9 9a000b97
...@@ -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) ->
......