Separate classes and the adapter / binder definitions into their own files.
Showing
8 changed files
with
733 additions
and
0 deletions
src/adapters.coffee
0 → 100644
1 | # The default `.` adapter thats comes with Rivets.js. Allows subscribing to | ||
2 | # properties on POJSOs, implemented in ES5 natives using | ||
3 | # `Object.defineProperty`. | ||
4 | Rivets.adapters['.'] = | ||
5 | id: '_rv' | ||
6 | counter: 0 | ||
7 | weakmap: {} | ||
8 | |||
9 | subscribe: (obj, keypath, callback) -> | ||
10 | unless obj[@id]? | ||
11 | obj[@id] = @counter++ | ||
12 | @weakmap[obj[@id]] = {} | ||
13 | |||
14 | map = @weakmap[obj[@id]] | ||
15 | |||
16 | unless map[keypath]? | ||
17 | map[keypath] = [] | ||
18 | value = obj[keypath] | ||
19 | |||
20 | Object.defineProperty obj, keypath, | ||
21 | get: -> value | ||
22 | set: (newValue) -> | ||
23 | if newValue isnt value | ||
24 | value = newValue | ||
25 | callback() for callback in map[keypath] | ||
26 | |||
27 | unless callback in map[keypath] | ||
28 | map[keypath].push callback | ||
29 | |||
30 | unsubscribe: (obj, keypath, callback) -> | ||
31 | callbacks = @weakmap[obj[@id]][keypath] | ||
32 | callbacks.splice callbacks.indexOf(callback), 1 | ||
33 | |||
34 | read: (obj, keypath) -> | ||
35 | obj[keypath] | ||
36 | |||
37 | publish: (obj, keypath, value) -> | ||
38 | obj[keypath] = value |
src/binders.coffee
0 → 100644
1 | # Core binders that are included with Rivets.js. | ||
2 | Rivets.binders.enabled = (el, value) -> | ||
3 | el.disabled = !value | ||
4 | |||
5 | Rivets.binders.disabled = (el, value) -> | ||
6 | el.disabled = !!value | ||
7 | |||
8 | Rivets.binders.checked = | ||
9 | publishes: true | ||
10 | bind: (el) -> | ||
11 | Rivets.Util.bindEvent el, 'change', @publish | ||
12 | unbind: (el) -> | ||
13 | Rivets.Util.unbindEvent el, 'change', @publish | ||
14 | routine: (el, value) -> | ||
15 | if el.type is 'radio' | ||
16 | el.checked = el.value?.toString() is value?.toString() | ||
17 | else | ||
18 | el.checked = !!value | ||
19 | |||
20 | Rivets.binders.unchecked = | ||
21 | publishes: true | ||
22 | bind: (el) -> | ||
23 | Rivets.Util.bindEvent el, 'change', @publish | ||
24 | unbind: (el) -> | ||
25 | Rivets.Util.unbindEvent el, 'change', @publish | ||
26 | routine: (el, value) -> | ||
27 | if el.type is 'radio' | ||
28 | el.checked = el.value?.toString() isnt value?.toString() | ||
29 | else | ||
30 | el.checked = !value | ||
31 | |||
32 | Rivets.binders.show = (el, value) -> | ||
33 | el.style.display = if value then '' else 'none' | ||
34 | |||
35 | Rivets.binders.hide = (el, value) -> | ||
36 | el.style.display = if value then 'none' else '' | ||
37 | |||
38 | Rivets.binders.html = (el, value) -> | ||
39 | el.innerHTML = if value? then value else '' | ||
40 | |||
41 | Rivets.binders.value = | ||
42 | publishes: true | ||
43 | bind: (el) -> | ||
44 | Rivets.Util.bindEvent el, 'change', @publish | ||
45 | unbind: (el) -> | ||
46 | Rivets.Util.unbindEvent el, 'change', @publish | ||
47 | routine: (el, value) -> | ||
48 | if window.jQuery? | ||
49 | el = jQuery el | ||
50 | |||
51 | if value?.toString() isnt el.val()?.toString() | ||
52 | el.val if value? then value else '' | ||
53 | else | ||
54 | if el.type is 'select-multiple' | ||
55 | o.selected = o.value in value for o in el if value? | ||
56 | else if value?.toString() isnt el.value?.toString() | ||
57 | el.value = if value? then value else '' | ||
58 | |||
59 | Rivets.binders.text = (el, value) -> | ||
60 | if el.innerText? | ||
61 | el.innerText = if value? then value else '' | ||
62 | else | ||
63 | el.textContent = if value? then value else '' | ||
64 | |||
65 | Rivets.binders.if = | ||
66 | block: true | ||
67 | |||
68 | bind: (el) -> | ||
69 | unless @marker? | ||
70 | attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-' | ||
71 | declaration = el.getAttribute attr | ||
72 | |||
73 | @marker = document.createComment " rivets: #{@type} #{declaration} " | ||
74 | |||
75 | el.removeAttribute attr | ||
76 | el.parentNode.insertBefore @marker, el | ||
77 | el.parentNode.removeChild el | ||
78 | |||
79 | unbind: -> | ||
80 | @nested?.unbind() | ||
81 | |||
82 | routine: (el, value) -> | ||
83 | if !!value is not @nested? | ||
84 | if value | ||
85 | models = {} | ||
86 | models[key] = model for key, model of @view.models | ||
87 | |||
88 | options = | ||
89 | binders: @view.options.binders | ||
90 | formatters: @view.options.formatters | ||
91 | adapters: @view.options.adapters | ||
92 | config: @view.options.config | ||
93 | |||
94 | (@nested = new Rivets.View(el, models, options)).bind() | ||
95 | @marker.parentNode.insertBefore el, @marker.nextSibling | ||
96 | else | ||
97 | el.parentNode.removeChild el | ||
98 | @nested.unbind() | ||
99 | delete @nested | ||
100 | |||
101 | update: (models) -> | ||
102 | @nested?.update models | ||
103 | |||
104 | Rivets.binders.unless = | ||
105 | block: true | ||
106 | |||
107 | bind: (el) -> | ||
108 | Rivets.binders.if.bind.call @, el | ||
109 | |||
110 | unbind: -> | ||
111 | Rivets.binders.if.unbind.call @ | ||
112 | |||
113 | routine: (el, value) -> | ||
114 | Rivets.binders.if.routine.call @, el, not value | ||
115 | |||
116 | update: (models) -> | ||
117 | Rivets.binders.if.update.call @, models | ||
118 | |||
119 | Rivets.binders['on-*'] = | ||
120 | function: true | ||
121 | |||
122 | unbind: (el) -> | ||
123 | Rivets.Util.unbindEvent el, @args[0], @handler if @handler | ||
124 | |||
125 | routine: (el, value) -> | ||
126 | Rivets.Util.unbindEvent el, @args[0], @handler if @handler | ||
127 | Rivets.Util.bindEvent el, @args[0], @handler = @eventHandler value | ||
128 | |||
129 | Rivets.binders['each-*'] = | ||
130 | block: true | ||
131 | |||
132 | bind: (el) -> | ||
133 | unless @marker? | ||
134 | attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-' | ||
135 | @marker = document.createComment " rivets: #{@type} " | ||
136 | @iterated = [] | ||
137 | |||
138 | el.removeAttribute attr | ||
139 | el.parentNode.insertBefore @marker, el | ||
140 | el.parentNode.removeChild el | ||
141 | |||
142 | unbind: (el) -> | ||
143 | view.unbind() for view in @iterated if @iterated? | ||
144 | |||
145 | routine: (el, collection) -> | ||
146 | modelName = @args[0] | ||
147 | collection = collection or [] | ||
148 | |||
149 | if @iterated.length > collection.length | ||
150 | for i in Array @iterated.length - collection.length | ||
151 | view = @iterated.pop() | ||
152 | view.unbind() | ||
153 | @marker.parentNode.removeChild view.els[0] | ||
154 | |||
155 | for model, index in collection | ||
156 | data = {} | ||
157 | data[modelName] = model | ||
158 | |||
159 | if not @iterated[index]? | ||
160 | for key, model of @view.models | ||
161 | data[key] ?= model | ||
162 | |||
163 | previous = if @iterated.length | ||
164 | @iterated[@iterated.length - 1].els[0] | ||
165 | else | ||
166 | @marker | ||
167 | |||
168 | options = | ||
169 | binders: @view.options.binders | ||
170 | formatters: @view.options.formatters | ||
171 | adapters: @view.options.adapters | ||
172 | config: {} | ||
173 | |||
174 | options.config[k] = v for k, v of @view.options.config | ||
175 | options.config.preloadData = true | ||
176 | |||
177 | template = el.cloneNode true | ||
178 | view = new Rivets.View(template, data, options) | ||
179 | view.bind() | ||
180 | @iterated.push view | ||
181 | |||
182 | @marker.parentNode.insertBefore template, previous.nextSibling | ||
183 | else if @iterated[index].models[modelName] isnt model | ||
184 | @iterated[index].update data | ||
185 | |||
186 | update: (models) -> | ||
187 | data = {} | ||
188 | |||
189 | for key, model of models | ||
190 | data[key] = model unless key is @args[0] | ||
191 | |||
192 | view.update data for view in @iterated | ||
193 | |||
194 | Rivets.binders['class-*'] = (el, value) -> | ||
195 | elClass = " #{el.className} " | ||
196 | |||
197 | if !value is (elClass.indexOf(" #{@args[0]} ") isnt -1) | ||
198 | el.className = if value | ||
199 | "#{el.className} #{@args[0]}" | ||
200 | else | ||
201 | elClass.replace(" #{@args[0]} ", ' ').trim() | ||
202 | |||
203 | Rivets.binders['*'] = (el, value) -> | ||
204 | if value | ||
205 | el.setAttribute @type, value | ||
206 | else | ||
207 | el.removeAttribute @type |
src/bindings.coffee
0 → 100644
1 | # Rivets.Binding | ||
2 | # -------------- | ||
3 | |||
4 | # A single binding between a model attribute and a DOM element. | ||
5 | class Rivets.Binding | ||
6 | # All information about the binding is passed into the constructor; the | ||
7 | # containing view, the DOM node, the type of binding, the model object and the | ||
8 | # keypath at which to listen for changes. | ||
9 | constructor: (@view, @el, @type, @keypath, @options = {}) -> | ||
10 | @formatters = @options.formatters || [] | ||
11 | @setBinders() | ||
12 | @setModel() | ||
13 | |||
14 | setBinders: => | ||
15 | unless @binder = @view.binders[@type] | ||
16 | for identifier, value of @view.binders | ||
17 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 | ||
18 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" | ||
19 | if regexp.test @type | ||
20 | @binder = value | ||
21 | @args = new RegExp("^#{identifier.replace('*', '(.+)')}$").exec @type | ||
22 | @args.shift() | ||
23 | |||
24 | @binder or= @view.binders['*'] | ||
25 | @binder = {routine: @binder} if @binder instanceof Function | ||
26 | |||
27 | setModel: => | ||
28 | interfaces = (k for k, v of @view.adapters) | ||
29 | tokens = Rivets.KeypathParser.parse @keypath, interfaces, '.' | ||
30 | |||
31 | @model = @view.models | ||
32 | @rootKey = tokens[0] | ||
33 | @key = tokens.pop() | ||
34 | |||
35 | for token, index in tokens | ||
36 | @model = @view.adapters[token.interface].read(@model, token.path) | ||
37 | |||
38 | # Applies all the current formatters to the supplied value and returns the | ||
39 | # formatted value. | ||
40 | formattedValue: (value) => | ||
41 | for formatter in @formatters | ||
42 | args = formatter.split /\s+/ | ||
43 | id = args.shift() | ||
44 | |||
45 | formatter = if @model[id] instanceof Function | ||
46 | @model[id] | ||
47 | else | ||
48 | @view.formatters[id] | ||
49 | |||
50 | if formatter?.read instanceof Function | ||
51 | value = formatter.read value, args... | ||
52 | else if formatter instanceof Function | ||
53 | value = formatter value, args... | ||
54 | |||
55 | value | ||
56 | |||
57 | # Returns an event handler for the binding around the supplied function. | ||
58 | eventHandler: (fn) => | ||
59 | handler = (binding = @).view.config.handler | ||
60 | (ev) -> handler.call fn, @, ev, binding | ||
61 | |||
62 | # Sets the value for the binding. This Basically just runs the binding routine | ||
63 | # with the suplied value formatted. | ||
64 | set: (value) => | ||
65 | value = if value instanceof Function and !@binder.function | ||
66 | @formattedValue value.call @model | ||
67 | else | ||
68 | @formattedValue value | ||
69 | |||
70 | @binder.routine?.call @, @el, value | ||
71 | |||
72 | # Syncs up the view binding with the model. | ||
73 | sync: => | ||
74 | @set @view.adapters[@key.interface].read @model, @key.path | ||
75 | |||
76 | # Publishes the value currently set on the input element back to the model. | ||
77 | publish: => | ||
78 | value = Rivets.Util.getInputValue @el | ||
79 | |||
80 | for formatter in @formatters.slice(0).reverse() | ||
81 | args = formatter.split /\s+/ | ||
82 | id = args.shift() | ||
83 | |||
84 | if @view.formatters[id]?.publish | ||
85 | value = @view.formatters[id].publish value, args... | ||
86 | |||
87 | @view.adapters[@key.interface].publish @model, @key.path, value | ||
88 | |||
89 | # Subscribes to the model for changes at the specified keypath. Bi-directional | ||
90 | # routines will also listen for changes on the element to propagate them back | ||
91 | # to the model. | ||
92 | bind: => | ||
93 | @binder.bind?.call @, @el | ||
94 | @view.adapters[@key.interface].subscribe @model, @key.path, @sync | ||
95 | @sync() if @view.config.preloadData | ||
96 | |||
97 | if @options.dependencies?.length | ||
98 | for dependency in @options.dependencies | ||
99 | if /^\./.test dependency | ||
100 | model = @model | ||
101 | keypath = dependency.substr 1 | ||
102 | else | ||
103 | dependency = dependency.split '.' | ||
104 | model = @view.models[dependency.shift()] | ||
105 | keypath = dependency.join '.' | ||
106 | |||
107 | @view.adapters['.'].subscribe model, keypath, @sync | ||
108 | |||
109 | # Unsubscribes from the model and the element. | ||
110 | unbind: => | ||
111 | @binder.unbind?.call @, @el | ||
112 | @view.adapters[@key.interface].unsubscribe @model, @key.path, @sync | ||
113 | |||
114 | if @options.dependencies?.length | ||
115 | for dependency in @options.dependencies | ||
116 | if /^\./.test dependency | ||
117 | model = @model | ||
118 | keypath = dependency.substr 1 | ||
119 | else | ||
120 | dependency = dependency.split '.' | ||
121 | model = @view.models[dependency.shift()] | ||
122 | keypath = dependency.join '.' | ||
123 | |||
124 | @view.adapters['.'].unsubscribe model, keypath, @sync | ||
125 | |||
126 | # Updates the binding's model from what is currently set on the view. Unbinds | ||
127 | # the old model first and then re-binds with the new model. | ||
128 | update: (models = {}) => | ||
129 | if models[@rootKey.path] | ||
130 | @view.adapters[@key.interface].unsubscribe @model, @key.path, @sync | ||
131 | @setModel() | ||
132 | @view.adapters[@key.interface].subscribe @model, @key.path, @sync | ||
133 | @sync() | ||
134 | |||
135 | @binder.update?.call @, models | ||
136 | |||
137 | # Rivets.ComponentBinding | ||
138 | # ----------------------- | ||
139 | |||
140 | # A component view encapsulated as a binding within it's parent view. | ||
141 | class Rivets.ComponentBinding extends Rivets.Binding | ||
142 | # Initializes a component binding for the specified view. The raw component | ||
143 | # element is passed in along with the component type. Attributes and scope | ||
144 | # inflections are determined based on the components defined attributes. | ||
145 | constructor: (@view, @el, @type) -> | ||
146 | @component = Rivets.components[@type] | ||
147 | @attributes = {} | ||
148 | @inflections = {} | ||
149 | |||
150 | for attribute in @el.attributes or [] | ||
151 | if attribute.name in @component.attributes | ||
152 | @attributes[attribute.name] = attribute.value | ||
153 | else | ||
154 | @inflections[attribute.name] = attribute.value | ||
155 | |||
156 | # Intercepts `Rivets.Binding::sync` since component bindings are not bound to | ||
157 | # a particular model to update it's value. | ||
158 | sync: -> | ||
159 | |||
160 | # Returns an object map using the component's scope inflections. | ||
161 | locals: (models = @view.models) => | ||
162 | result = {} | ||
163 | |||
164 | for key, inverse of @inflections | ||
165 | result[key] = (result[key] or models)[path] for path in inverse.split '.' | ||
166 | |||
167 | result[key] ?= model for key, model of models | ||
168 | result | ||
169 | |||
170 | # Intercepts `Rivets.Binding::update` to be called on `@componentView` with a | ||
171 | # localized map of the models. | ||
172 | update: (models) => | ||
173 | @componentView?.update @locals models | ||
174 | |||
175 | # Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized | ||
176 | # map of models from the root view. Bind `@componentView` on subsequent calls. | ||
177 | bind: => | ||
178 | if @componentView? | ||
179 | @componentView?.bind() | ||
180 | else | ||
181 | el = @component.build.call @attributes | ||
182 | (@componentView = new Rivets.View(el, @locals(), @view.options)).bind() | ||
183 | @el.parentNode.replaceChild el, @el | ||
184 | |||
185 | # Intercept `Rivets.Binding::unbind` to be called on `@componentView`. | ||
186 | unbind: => | ||
187 | @componentView?.unbind() | ||
188 | |||
189 | # Rivets.TextBinding | ||
190 | # ----------------------- | ||
191 | |||
192 | # A text node binding, defined internally to deal with text and element node | ||
193 | # differences while avoiding it being overwritten. | ||
194 | class Rivets.TextBinding extends Rivets.Binding | ||
195 | # Initializes a text binding for the specified view and text node. | ||
196 | constructor: (@view, @el, @type, @keypath, @options = {}) -> | ||
197 | interfaces = (k for k, v of @view.adapters) | ||
198 | tokens = Rivets.KeypathParser.parse(@keypath, interfaces, '.') | ||
199 | @formatters = @options.formatters || [] | ||
200 | @setModel() | ||
201 | |||
202 | # A standard routine binder used for text node bindings. | ||
203 | binder: | ||
204 | routine: (node, value) -> | ||
205 | node.data = value ? '' | ||
206 | |||
207 | # Wrap the call to `sync` in fat-arrow to avoid function context issues. | ||
208 | sync: => | ||
209 | super |
src/export.coffee
0 → 100644
1 | # Rivets.factory | ||
2 | # -------------- | ||
3 | |||
4 | # Rivets.js module factory. | ||
5 | Rivets.factory = (exports) -> | ||
6 | # Exposes the full Rivets namespace. This is mainly used for isolated testing. | ||
7 | exports._ = Rivets | ||
8 | |||
9 | # Exposes the binders object. | ||
10 | exports.binders = Rivets.binders | ||
11 | |||
12 | # Exposes the components object. | ||
13 | exports.components = Rivets.components | ||
14 | |||
15 | # Exposes the formatters object. | ||
16 | exports.formatters = Rivets.formatters | ||
17 | |||
18 | # Exposes the adapters object. | ||
19 | exports.adapters = Rivets.adapters | ||
20 | |||
21 | # Exposes the config object. | ||
22 | exports.config = Rivets.config | ||
23 | |||
24 | # Merges an object literal onto the config object. | ||
25 | exports.configure = (options={}) -> | ||
26 | for property, value of options | ||
27 | Rivets.config[property] = value | ||
28 | return | ||
29 | |||
30 | # Binds a set of model objects to a parent DOM element and returns a | ||
31 | # `Rivets.View` instance. | ||
32 | exports.bind = (el, models = {}, options = {}) -> | ||
33 | view = new Rivets.View(el, models, options) | ||
34 | view.bind() | ||
35 | view | ||
36 | |||
37 | # Exports Rivets.js for CommonJS, AMD and the browser. | ||
38 | if typeof exports == 'object' | ||
39 | Rivets.factory(exports) | ||
40 | else if typeof define == 'function' && define.amd | ||
41 | define ['exports'], (exports) -> | ||
42 | Rivets.factory(@rivets = exports) | ||
43 | return exports | ||
44 | else | ||
45 | Rivets.factory(@rivets = {}) |
src/parsers.coffee
0 → 100644
1 | # Rivets.KeypathParser | ||
2 | # -------------------- | ||
3 | |||
4 | # Parser and tokenizer for keypaths in binding declarations. | ||
5 | class Rivets.KeypathParser | ||
6 | # Parses the keypath and returns a set of adapter interface + path tokens. | ||
7 | @parse: (keypath, interfaces, root) -> | ||
8 | tokens = [] | ||
9 | current = {interface: root, path: ''} | ||
10 | |||
11 | for index, char of keypath | ||
12 | if char in interfaces | ||
13 | tokens.push current | ||
14 | current = {interface: char, path: ''} | ||
15 | else | ||
16 | current.path += char | ||
17 | |||
18 | tokens.push current | ||
19 | tokens | ||
20 | |||
21 | # Rivets.TextTemplateParser | ||
22 | # ------------------------- | ||
23 | |||
24 | # Rivets.js text template parser and tokenizer for mustache-style text content | ||
25 | # binding declarations. | ||
26 | class Rivets.TextTemplateParser | ||
27 | @types: | ||
28 | text: 0 | ||
29 | binding: 1 | ||
30 | |||
31 | # Parses the template and returns a set of tokens, separating static portions | ||
32 | # of text from binding declarations. | ||
33 | @parse: (template, delimiters) -> | ||
34 | tokens = [] | ||
35 | length = template.length | ||
36 | index = 0 | ||
37 | lastIndex = 0 | ||
38 | |||
39 | while lastIndex < length | ||
40 | index = template.indexOf delimiters[0], lastIndex | ||
41 | |||
42 | if index < 0 | ||
43 | tokens.push type: @types.text, value: template.slice lastIndex | ||
44 | break | ||
45 | else | ||
46 | if index > 0 and lastIndex < index | ||
47 | tokens.push type: @types.text, value: template.slice lastIndex, index | ||
48 | |||
49 | lastIndex = index + 2 | ||
50 | index = template.indexOf delimiters[1], lastIndex | ||
51 | |||
52 | if index < 0 | ||
53 | substring = template.slice lastIndex - 2 | ||
54 | lastToken = tokens[tokens.length - 1] | ||
55 | |||
56 | if lastToken?.type is @types.text | ||
57 | lastToken.value += substring | ||
58 | else | ||
59 | tokens.push type: @types.text, value: substring | ||
60 | |||
61 | break | ||
62 | |||
63 | value = template.slice(lastIndex, index).trim() | ||
64 | tokens.push type: @types.binding, value: value | ||
65 | lastIndex = index + 2 | ||
66 | |||
67 | tokens |
This diff is collapsed.
Click to expand it.
src/util.coffee
0 → 100644
1 | # Rivets.Util | ||
2 | # ----------- | ||
3 | |||
4 | # Houses common utility functions used internally by Rivets.js. | ||
5 | Rivets.Util = | ||
6 | # Create a single DOM event binding. | ||
7 | bindEvent: (el, event, handler) -> | ||
8 | if window.jQuery? | ||
9 | el = jQuery el | ||
10 | if el.on? then el.on event, handler else el.bind event, handler | ||
11 | else if window.addEventListener? | ||
12 | el.addEventListener event, handler, false | ||
13 | else | ||
14 | event = 'on' + event | ||
15 | el.attachEvent event, handler | ||
16 | |||
17 | # Remove a single DOM event binding. | ||
18 | unbindEvent: (el, event, handler) -> | ||
19 | if window.jQuery? | ||
20 | el = jQuery el | ||
21 | if el.off? then el.off event, handler else el.unbind event, handler | ||
22 | else if window.removeEventListener? | ||
23 | el.removeEventListener event, handler, false | ||
24 | else | ||
25 | event = 'on' + event | ||
26 | el.detachEvent event, handler | ||
27 | |||
28 | # Get the current value of an input node. | ||
29 | getInputValue: (el) -> | ||
30 | if window.jQuery? | ||
31 | el = jQuery el | ||
32 | |||
33 | switch el[0].type | ||
34 | when 'checkbox' then el.is ':checked' | ||
35 | else el.val() | ||
36 | else | ||
37 | switch el.type | ||
38 | when 'checkbox' then el.checked | ||
39 | when 'select-multiple' then o.value for o in el when o.selected | ||
40 | else el.value |
src/view.coffee
0 → 100644
1 | # Rivets.View | ||
2 | # ----------- | ||
3 | |||
4 | # A collection of bindings built from a set of parent nodes. | ||
5 | class Rivets.View | ||
6 | # The DOM elements and the model objects for binding are passed into the | ||
7 | # constructor along with any local options that should be used throughout the | ||
8 | # context of the view and it's bindings. | ||
9 | constructor: (@els, @models, @options = {}) -> | ||
10 | @els = [@els] unless (@els.jquery || @els instanceof Array) | ||
11 | |||
12 | for option in ['config', 'binders', 'formatters', 'adapters'] | ||
13 | @[option] = {} | ||
14 | @[option][k] = v for k, v of @options[option] if @options[option] | ||
15 | @[option][k] ?= v for k, v of Rivets[option] | ||
16 | |||
17 | @build() | ||
18 | |||
19 | # Regular expression used to match binding attributes. | ||
20 | bindingRegExp: => | ||
21 | prefix = @config.prefix | ||
22 | if prefix then new RegExp("^data-#{prefix}-") else /^data-/ | ||
23 | |||
24 | # Regular expression used to match component nodes. | ||
25 | componentRegExp: => | ||
26 | new RegExp "^#{@config.prefix?.toUpperCase() ? 'RV'}-" | ||
27 | |||
28 | # Parses the DOM tree and builds `Rivets.Binding` instances for every matched | ||
29 | # binding declaration. | ||
30 | build: => | ||
31 | @bindings = [] | ||
32 | skipNodes = [] | ||
33 | bindingRegExp = @bindingRegExp() | ||
34 | componentRegExp = @componentRegExp() | ||
35 | |||
36 | buildBinding = (binding, node, type, declaration) => | ||
37 | options = {} | ||
38 | |||
39 | pipes = (pipe.trim() for pipe in declaration.split '|') | ||
40 | context = (ctx.trim() for ctx in pipes.shift().split '<') | ||
41 | keypath = context.shift() | ||
42 | |||
43 | options.formatters = pipes | ||
44 | |||
45 | if dependencies = context.shift() | ||
46 | options.dependencies = dependencies.split /\s+/ | ||
47 | |||
48 | @bindings.push new Rivets[binding] @, node, type, keypath, options | ||
49 | |||
50 | parse = (node) => | ||
51 | unless node in skipNodes | ||
52 | if node.nodeType is Node.TEXT_NODE | ||
53 | parser = Rivets.TextTemplateParser | ||
54 | |||
55 | if delimiters = @config.templateDelimiters | ||
56 | if (tokens = parser.parse(node.data, delimiters)).length | ||
57 | unless tokens.length is 1 and tokens[0].type is parser.types.text | ||
58 | [startToken, restTokens...] = tokens | ||
59 | node.data = startToken.value | ||
60 | |||
61 | if startToken.type is 0 | ||
62 | node.data = startToken.value | ||
63 | else | ||
64 | buildBinding 'TextBinding', node, null, startToken.value | ||
65 | |||
66 | for token in restTokens | ||
67 | text = document.createTextNode token.value | ||
68 | node.parentNode.appendChild text | ||
69 | |||
70 | if token.type is 1 | ||
71 | buildBinding 'TextBinding', text, null, token.value | ||
72 | else if componentRegExp.test node.tagName | ||
73 | type = node.tagName.replace(componentRegExp, '').toLowerCase() | ||
74 | @bindings.push new Rivets.ComponentBinding @, node, type | ||
75 | |||
76 | else if node.attributes? | ||
77 | for attribute in node.attributes | ||
78 | if bindingRegExp.test attribute.name | ||
79 | type = attribute.name.replace bindingRegExp, '' | ||
80 | unless binder = @binders[type] | ||
81 | for identifier, value of @binders | ||
82 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 | ||
83 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" | ||
84 | if regexp.test type | ||
85 | binder = value | ||
86 | |||
87 | binder or= @binders['*'] | ||
88 | |||
89 | if binder.block | ||
90 | skipNodes.push n for n in node.childNodes | ||
91 | attributes = [attribute] | ||
92 | |||
93 | for attribute in attributes or node.attributes | ||
94 | if bindingRegExp.test attribute.name | ||
95 | type = attribute.name.replace bindingRegExp, '' | ||
96 | buildBinding 'Binding', node, type, attribute.value | ||
97 | |||
98 | parse childNode for childNode in node.childNodes | ||
99 | |||
100 | parse el for el in @els | ||
101 | |||
102 | return | ||
103 | |||
104 | # Returns an array of bindings where the supplied function evaluates to true. | ||
105 | select: (fn) => | ||
106 | binding for binding in @bindings when fn binding | ||
107 | |||
108 | # Binds all of the current bindings for this view. | ||
109 | bind: => | ||
110 | binding.bind() for binding in @bindings | ||
111 | |||
112 | # Unbinds all of the current bindings for this view. | ||
113 | unbind: => | ||
114 | binding.unbind() for binding in @bindings | ||
115 | |||
116 | # Syncs up the view with the model by running the routines on all bindings. | ||
117 | sync: => | ||
118 | binding.sync() for binding in @bindings | ||
119 | |||
120 | # Publishes the input values from the view back to the model (reverse sync). | ||
121 | publish: => | ||
122 | binding.publish() for binding in @select (b) -> b.binder.publishes | ||
123 | |||
124 | # Updates the view's models along with any affected bindings. | ||
125 | update: (models = {}) => | ||
126 | @models[key] = model for key, model of models | ||
127 | binding.update models for binding in @bindings |
-
Please register or sign in to post a comment