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,15 +87,16 @@ class Rivets.Binding ...@@ -85,15 +87,16 @@ 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: =>
88 Rivets.config.adapter.unsubscribe @model, @keypath, @set 90 unless @options.bypass
91 Rivets.config.adapter.unsubscribe @model, @keypath, @set
89 92
90 if @options.dependencies?.length 93 if @options.dependencies?.length
91 for keypath in @options.dependencies 94 for keypath in @options.dependencies
92 callback = @dependencyCallbacks[keypath] 95 callback = @dependencyCallbacks[keypath]
93 Rivets.config.adapter.unsubscribe @model, keypath, callback 96 Rivets.config.adapter.unsubscribe @model, keypath, callback
94 97
95 if @type in @bidirectionals 98 if @type in @bidirectionals
96 @el.removeEventListener 'change', @publish 99 @el.removeEventListener 'change', @publish
97 100
98 # A collection of bindings built from a set of parent elements. 101 # A collection of bindings built from a set of parent elements.
99 class Rivets.View 102 class Rivets.View
...@@ -111,32 +114,57 @@ class Rivets.View ...@@ -111,32 +114,57 @@ 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) =>
118 for attribute in node.attributes 124 unless node in skipNodes
119 if bindingRegExp.test attribute.name 125 for attribute in node.attributes
120 options = {} 126 if bindingRegExp.test attribute.name
121 127 type = attribute.name.replace bindingRegExp, ''
122 type = attribute.name.replace bindingRegExp, '' 128
123 pipes = (pipe.trim() for pipe in attribute.value.split '|') 129 if iterationRegExp.test type
124 context = (ctx.trim() for ctx in pipes.shift().split '>') 130 unless @models[type.replace iterationRegExp, '']
125 path = context.shift() 131 skipNodes.push n for n in node.getElementsByTagName '*'
126 splitPath = path.split /\.|:/ 132 iterator = [attribute]
127 options.formatters = pipes 133
128 model = @models[splitPath.shift()] 134 for attribute in iterator or node.attributes
129 options.bypass = path.indexOf(":") != -1 135 if bindingRegExp.test attribute.name
130 keypath = splitPath.join() 136 options = {}
131 137
132 if dependencies = context.shift() 138 type = attribute.name.replace bindingRegExp, ''
133 options.dependencies = dependencies.split /\s+/ 139 pipes = (pipe.trim() for pipe in attribute.value.split '|')
134 140 context = (ctx.trim() for ctx in pipes.shift().split '>')
135 if eventRegExp.test type 141 path = context.shift()
136 type = type.replace eventRegExp, '' 142 splitPath = path.split /\.|:/
137 options.special = "event" 143 options.formatters = pipes
138 144 model = @models[splitPath.shift()]
139 @bindings.push new Rivets.Binding node, type, model, keypath, options 145 options.bypass = path.indexOf(":") != -1
146 keypath = splitPath.join()
147
148 if model
149 if dependencies = context.shift()
150 options.dependencies = dependencies.split /\s+/
151
152 if eventRegExp.test type
153 type = type.replace eventRegExp, ''
154 options.special = "event"
155
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) ->
......