Separate classes and the adapter / binder definitions into their own files.
Showing
8 changed files
with
753 additions
and
792 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 |
1 | # Rivets.js | ||
2 | # ========= | ||
3 | |||
4 | # > version: 0.5.12 | ||
5 | # > author: Michael Richards | ||
6 | # > license: MIT | ||
7 | # > | ||
8 | # > http://rivetsjs.com/ | ||
9 | |||
10 | # --- | ||
11 | |||
12 | # The Rivets namespace. | 1 | # The Rivets namespace. |
13 | Rivets = {} | 2 | Rivets = |
14 | 3 | # Binder definitions, publicly accessible on `module.binders`. Can be | |
15 | # Polyfill For `String::trim`. | 4 | # overridden globally or local to a `Rivets.View` instance. |
16 | unless String::trim then String::trim = -> @replace /^\s+|\s+$/g, '' | 5 | binders: {} |
17 | 6 | ||
18 | # Rivets.Binding | 7 | # Component definitions, publicly accessible on `module.components`. Can be |
19 | # -------------- | 8 | # overridden globally or local to a `Rivets.View` instance. |
20 | 9 | components: {} | |
21 | # A single binding between a model attribute and a DOM element. | 10 | |
22 | class Rivets.Binding | 11 | # Formatter definitions, publicly accessible on `module.formatters`. Can be |
23 | # All information about the binding is passed into the constructor; the | 12 | # overridden globally or local to a `Rivets.View` instance. |
24 | # containing view, the DOM node, the type of binding, the model object and the | 13 | formatters: {} |
25 | # keypath at which to listen for changes. | 14 | |
26 | constructor: (@view, @el, @type, @keypath, @options = {}) -> | 15 | # Adapter definitions, publicly accessible on `module.adapters`. Can be |
27 | @formatters = @options.formatters || [] | 16 | # overridden globally or local to a `Rivets.View` instance. |
28 | @setBinders() | 17 | adapters: {} |
29 | @setModel() | 18 | |
30 | 19 | # The default configuration, publicly accessible on `module.config`. Can be | |
31 | setBinders: => | 20 | # overridden globally or local to a `Rivets.View` instance. |
32 | unless @binder = @view.binders[type] | 21 | config: |
33 | for identifier, value of @view.binders | ||
34 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 | ||
35 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" | ||
36 | if regexp.test type | ||
37 | @binder = value | ||
38 | @args = new RegExp("^#{identifier.replace('*', '(.+)')}$").exec type | ||
39 | @args.shift() | ||
40 | |||
41 | @binder or= @view.binders['*'] | ||
42 | @binder = {routine: @binder} if @binder instanceof Function | ||
43 | |||
44 | setModel: => | ||
45 | interfaces = (k for k, v of @view.adapters) | ||
46 | tokens = Rivets.KeypathParser.parse @keypath, interfaces, '.' | ||
47 | |||
48 | @model = @view.models | ||
49 | @rootKey = tokens[0] | ||
50 | @key = tokens.pop() | ||
51 | |||
52 | for token, index in tokens | ||
53 | @model = @view.adapters[token.interface].read(@model, token.path) | ||
54 | |||
55 | # Applies all the current formatters to the supplied value and returns the | ||
56 | # formatted value. | ||
57 | formattedValue: (value) => | ||
58 | for formatter in @formatters | ||
59 | args = formatter.split /\s+/ | ||
60 | id = args.shift() | ||
61 | |||
62 | formatter = if @model[id] instanceof Function | ||
63 | @model[id] | ||
64 | else | ||
65 | @view.formatters[id] | ||
66 | |||
67 | if formatter?.read instanceof Function | ||
68 | value = formatter.read value, args... | ||
69 | else if formatter instanceof Function | ||
70 | value = formatter value, args... | ||
71 | |||
72 | value | ||
73 | |||
74 | # Returns an event handler for the binding around the supplied function. | ||
75 | eventHandler: (fn) => | ||
76 | handler = (binding = @).view.config.handler | ||
77 | (ev) -> handler.call fn, @, ev, binding | ||
78 | |||
79 | # Sets the value for the binding. This Basically just runs the binding routine | ||
80 | # with the suplied value formatted. | ||
81 | set: (value) => | ||
82 | value = if value instanceof Function and !@binder.function | ||
83 | @formattedValue value.call @model | ||
84 | else | ||
85 | @formattedValue value | ||
86 | |||
87 | @binder.routine?.call @, @el, value | ||
88 | |||
89 | # Syncs up the view binding with the model. | ||
90 | sync: => | ||
91 | @set @view.adapters[@key.interface].read @model, @key.path | ||
92 | |||
93 | # Publishes the value currently set on the input element back to the model. | ||
94 | publish: => | ||
95 | value = Rivets.Util.getInputValue @el | ||
96 | |||
97 | for formatter in @formatters.slice(0).reverse() | ||
98 | args = formatter.split /\s+/ | ||
99 | id = args.shift() | ||
100 | |||
101 | if @view.formatters[id]?.publish | ||
102 | value = @view.formatters[id].publish value, args... | ||
103 | |||
104 | @view.adapters[@key.interface].publish @model, @key.path, value | ||
105 | |||
106 | # Subscribes to the model for changes at the specified keypath. Bi-directional | ||
107 | # routines will also listen for changes on the element to propagate them back | ||
108 | # to the model. | ||
109 | bind: => | ||
110 | @binder.bind?.call @, @el | ||
111 | @view.adapters[@key.interface].subscribe @model, @key.path, @sync | ||
112 | @sync() if @view.config.preloadData | ||
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['.'].subscribe model, keypath, @sync | ||
125 | |||
126 | # Unsubscribes from the model and the element. | ||
127 | unbind: => | ||
128 | @binder.unbind?.call @, @el | ||
129 | @view.adapters[@key.interface].unsubscribe @model, @key.path, @sync | ||
130 | |||
131 | if @options.dependencies?.length | ||
132 | for dependency in @options.dependencies | ||
133 | if /^\./.test dependency | ||
134 | model = @model | ||
135 | keypath = dependency.substr 1 | ||
136 | else | ||
137 | dependency = dependency.split '.' | ||
138 | model = @view.models[dependency.shift()] | ||
139 | keypath = dependency.join '.' | ||
140 | |||
141 | @view.adapters['.'].unsubscribe model, keypath, @sync | ||
142 | |||
143 | # Updates the binding's model from what is currently set on the view. Unbinds | ||
144 | # the old model first and then re-binds with the new model. | ||
145 | update: (models = {}) => | ||
146 | if models[@rootKey.path] | ||
147 | @view.adapters[@key.interface].unsubscribe @model, @key.path, @sync | ||
148 | @setModel() | ||
149 | @view.adapters[@key.interface].subscribe @model, @key.path, @sync | ||
150 | @sync() | ||
151 | |||
152 | @binder.update?.call @, models | ||
153 | |||
154 | # Rivets.ComponentBinding | ||
155 | # ----------------------- | ||
156 | |||
157 | # A component view encapsulated as a binding within it's parent view. | ||
158 | class Rivets.ComponentBinding extends Rivets.Binding | ||
159 | # Initializes a component binding for the specified view. The raw component | ||
160 | # element is passed in along with the component type. Attributes and scope | ||
161 | # inflections are determined based on the components defined attributes. | ||
162 | constructor: (@view, @el, @type) -> | ||
163 | @component = Rivets.components[@type] | ||
164 | @attributes = {} | ||
165 | @inflections = {} | ||
166 | |||
167 | for attribute in @el.attributes or [] | ||
168 | if attribute.name in @component.attributes | ||
169 | @attributes[attribute.name] = attribute.value | ||
170 | else | ||
171 | @inflections[attribute.name] = attribute.value | ||
172 | |||
173 | # Intercepts `Rivets.Binding::sync` since component bindings are not bound to | ||
174 | # a particular model to update it's value. | ||
175 | sync: -> | ||
176 | |||
177 | # Returns an object map using the component's scope inflections. | ||
178 | locals: (models = @view.models) => | ||
179 | result = {} | ||
180 | |||
181 | for key, inverse of @inflections | ||
182 | result[key] = (result[key] or models)[path] for path in inverse.split '.' | ||
183 | |||
184 | result[key] ?= model for key, model of models | ||
185 | result | ||
186 | |||
187 | # Intercepts `Rivets.Binding::update` to be called on `@componentView` with a | ||
188 | # localized map of the models. | ||
189 | update: (models) => | ||
190 | @componentView?.update @locals models | ||
191 | |||
192 | # Intercepts `Rivets.Binding::bind` to build `@componentView` with a localized | ||
193 | # map of models from the root view. Bind `@componentView` on subsequent calls. | ||
194 | bind: => | ||
195 | if @componentView? | ||
196 | @componentView?.bind() | ||
197 | else | ||
198 | el = @component.build.call @attributes | ||
199 | (@componentView = new Rivets.View(el, @locals(), @view.options)).bind() | ||
200 | @el.parentNode.replaceChild el, @el | ||
201 | |||
202 | # Intercept `Rivets.Binding::unbind` to be called on `@componentView`. | ||
203 | unbind: => | ||
204 | @componentView?.unbind() | ||
205 | |||
206 | # Rivets.TextBinding | ||
207 | # ----------------------- | ||
208 | |||
209 | # A text node binding, defined internally to deal with text and element node | ||
210 | # differences while avoiding it being overwritten. | ||
211 | class Rivets.TextBinding extends Rivets.Binding | ||
212 | # Initializes a text binding for the specified view and text node. | ||
213 | constructor: (@view, @el, @type, @keypath, @options = {}) -> | ||
214 | interfaces = (k for k, v of @view.adapters) | ||
215 | tokens = Rivets.KeypathParser.parse(@keypath, interfaces, '.') | ||
216 | @formatters = @options.formatters || [] | ||
217 | @setModel() | ||
218 | |||
219 | # A standard routine binder used for text node bindings. | ||
220 | binder: | ||
221 | routine: (node, value) -> | ||
222 | node.data = value ? '' | ||
223 | |||
224 | # Wrap the call to `sync` in fat-arrow to avoid function context issues. | ||
225 | sync: => | ||
226 | super | ||
227 | |||
228 | # Rivets.View | ||
229 | # ----------- | ||
230 | |||
231 | # A collection of bindings built from a set of parent nodes. | ||
232 | class Rivets.View | ||
233 | # The DOM elements and the model objects for binding are passed into the | ||
234 | # constructor along with any local options that should be used throughout the | ||
235 | # context of the view and it's bindings. | ||
236 | constructor: (@els, @models, @options = {}) -> | ||
237 | @els = [@els] unless (@els.jquery || @els instanceof Array) | ||
238 | |||
239 | for option in ['config', 'binders', 'formatters', 'adapters'] | ||
240 | @[option] = {} | ||
241 | @[option][k] = v for k, v of @options[option] if @options[option] | ||
242 | @[option][k] ?= v for k, v of Rivets[option] | ||
243 | |||
244 | @build() | ||
245 | |||
246 | # Regular expression used to match binding attributes. | ||
247 | bindingRegExp: => | ||
248 | prefix = @config.prefix | ||
249 | if prefix then new RegExp("^data-#{prefix}-") else /^data-/ | ||
250 | |||
251 | # Regular expression used to match component nodes. | ||
252 | componentRegExp: => | ||
253 | new RegExp "^#{@config.prefix?.toUpperCase() ? 'RV'}-" | ||
254 | |||
255 | # Parses the DOM tree and builds `Rivets.Binding` instances for every matched | ||
256 | # binding declaration. | ||
257 | build: => | ||
258 | @bindings = [] | ||
259 | skipNodes = [] | ||
260 | bindingRegExp = @bindingRegExp() | ||
261 | componentRegExp = @componentRegExp() | ||
262 | |||
263 | buildBinding = (binding, node, type, declaration) => | ||
264 | options = {} | ||
265 | |||
266 | pipes = (pipe.trim() for pipe in declaration.split '|') | ||
267 | context = (ctx.trim() for ctx in pipes.shift().split '<') | ||
268 | keypath = context.shift() | ||
269 | |||
270 | options.formatters = pipes | ||
271 | |||
272 | if dependencies = context.shift() | ||
273 | options.dependencies = dependencies.split /\s+/ | ||
274 | |||
275 | @bindings.push new Rivets[binding] @, node, type, keypath, options | ||
276 | |||
277 | parse = (node) => | ||
278 | unless node in skipNodes | ||
279 | if node.nodeType is Node.TEXT_NODE | ||
280 | parser = Rivets.TextTemplateParser | ||
281 | |||
282 | if delimiters = @config.templateDelimiters | ||
283 | if (tokens = parser.parse(node.data, delimiters)).length | ||
284 | unless tokens.length is 1 and tokens[0].type is parser.types.text | ||
285 | [startToken, restTokens...] = tokens | ||
286 | node.data = startToken.value | ||
287 | |||
288 | if startToken.type is 0 | ||
289 | node.data = startToken.value | ||
290 | else | ||
291 | buildBinding 'TextBinding', node, null, startToken.value | ||
292 | |||
293 | for token in restTokens | ||
294 | text = document.createTextNode token.value | ||
295 | node.parentNode.appendChild text | ||
296 | |||
297 | if token.type is 1 | ||
298 | buildBinding 'TextBinding', text, null, token.value | ||
299 | else if componentRegExp.test node.tagName | ||
300 | type = node.tagName.replace(componentRegExp, '').toLowerCase() | ||
301 | @bindings.push new Rivets.ComponentBinding @, node, type | ||
302 | |||
303 | else if node.attributes? | ||
304 | for attribute in node.attributes | ||
305 | if bindingRegExp.test attribute.name | ||
306 | type = attribute.name.replace bindingRegExp, '' | ||
307 | unless binder = @binders[type] | ||
308 | for identifier, value of @binders | ||
309 | if identifier isnt '*' and identifier.indexOf('*') isnt -1 | ||
310 | regexp = new RegExp "^#{identifier.replace('*', '.+')}$" | ||
311 | if regexp.test type | ||
312 | binder = value | ||
313 | |||
314 | binder or= @binders['*'] | ||
315 | |||
316 | if binder.block | ||
317 | skipNodes.push n for n in node.childNodes | ||
318 | attributes = [attribute] | ||
319 | |||
320 | for attribute in attributes or node.attributes | ||
321 | if bindingRegExp.test attribute.name | ||
322 | type = attribute.name.replace bindingRegExp, '' | ||
323 | buildBinding 'Binding', node, type, attribute.value | ||
324 | |||
325 | parse childNode for childNode in node.childNodes | ||
326 | |||
327 | parse el for el in @els | ||
328 | |||
329 | return | ||
330 | |||
331 | # Returns an array of bindings where the supplied function evaluates to true. | ||
332 | select: (fn) => | ||
333 | binding for binding in @bindings when fn binding | ||
334 | |||
335 | # Binds all of the current bindings for this view. | ||
336 | bind: => | ||
337 | binding.bind() for binding in @bindings | ||
338 | |||
339 | # Unbinds all of the current bindings for this view. | ||
340 | unbind: => | ||
341 | binding.unbind() for binding in @bindings | ||
342 | |||
343 | # Syncs up the view with the model by running the routines on all bindings. | ||
344 | sync: => | ||
345 | binding.sync() for binding in @bindings | ||
346 | |||
347 | # Publishes the input values from the view back to the model (reverse sync). | ||
348 | publish: => | ||
349 | binding.publish() for binding in @select (b) -> b.binder.publishes | ||
350 | |||
351 | # Updates the view's models along with any affected bindings. | ||
352 | update: (models = {}) => | ||
353 | @models[key] = model for key, model of models | ||
354 | binding.update models for binding in @bindings | ||
355 | |||
356 | # Rivets.KeypathParser | ||
357 | # -------------------- | ||
358 | |||
359 | # Parser and tokenizer for keypaths in binding declarations. | ||
360 | class Rivets.KeypathParser | ||
361 | # Parses the keypath and returns a set of adapter interface + path tokens. | ||
362 | @parse: (keypath, interfaces, root) -> | ||
363 | tokens = [] | ||
364 | current = {interface: root, path: ''} | ||
365 | |||
366 | for index, char of keypath | ||
367 | if char in interfaces | ||
368 | tokens.push current | ||
369 | current = {interface: char, path: ''} | ||
370 | else | ||
371 | current.path += char | ||
372 | |||
373 | tokens.push current | ||
374 | tokens | ||
375 | |||
376 | # Rivets.TextTemplateParser | ||
377 | # ------------------------- | ||
378 | |||
379 | # Rivets.js text template parser and tokenizer for mustache-style text content | ||
380 | # binding declarations. | ||
381 | class Rivets.TextTemplateParser | ||
382 | @types: | ||
383 | text: 0 | ||
384 | binding: 1 | ||
385 | |||
386 | # Parses the template and returns a set of tokens, separating static portions | ||
387 | # of text from binding declarations. | ||
388 | @parse: (template, delimiters) -> | ||
389 | tokens = [] | ||
390 | length = template.length | ||
391 | index = 0 | ||
392 | lastIndex = 0 | ||
393 | |||
394 | while lastIndex < length | ||
395 | index = template.indexOf delimiters[0], lastIndex | ||
396 | |||
397 | if index < 0 | ||
398 | tokens.push type: @types.text, value: template.slice lastIndex | ||
399 | break | ||
400 | else | ||
401 | if index > 0 and lastIndex < index | ||
402 | tokens.push type: @types.text, value: template.slice lastIndex, index | ||
403 | |||
404 | lastIndex = index + 2 | ||
405 | index = template.indexOf delimiters[1], lastIndex | ||
406 | |||
407 | if index < 0 | ||
408 | substring = template.slice lastIndex - 2 | ||
409 | lastToken = tokens[tokens.length - 1] | ||
410 | |||
411 | if lastToken?.type is @types.text | ||
412 | lastToken.value += substring | ||
413 | else | ||
414 | tokens.push type: @types.text, value: substring | ||
415 | |||
416 | break | ||
417 | |||
418 | value = template.slice(lastIndex, index).trim() | ||
419 | tokens.push type: @types.binding, value: value | ||
420 | lastIndex = index + 2 | ||
421 | |||
422 | tokens | ||
423 | |||
424 | # Rivets.Util | ||
425 | # ----------- | ||
426 | |||
427 | # Houses common utility functions used internally by Rivets.js. | ||
428 | Rivets.Util = | ||
429 | # Create a single DOM event binding. | ||
430 | bindEvent: (el, event, handler) -> | ||
431 | if window.jQuery? | ||
432 | el = jQuery el | ||
433 | if el.on? then el.on event, handler else el.bind event, handler | ||
434 | else if window.addEventListener? | ||
435 | el.addEventListener event, handler, false | ||
436 | else | ||
437 | event = 'on' + event | ||
438 | el.attachEvent event, handler | ||
439 | |||
440 | # Remove a single DOM event binding. | ||
441 | unbindEvent: (el, event, handler) -> | ||
442 | if window.jQuery? | ||
443 | el = jQuery el | ||
444 | if el.off? then el.off event, handler else el.unbind event, handler | ||
445 | else if window.removeEventListener? | ||
446 | el.removeEventListener event, handler, false | ||
447 | else | ||
448 | event = 'on' + event | ||
449 | el.detachEvent event, handler | ||
450 | |||
451 | # Get the current value of an input node. | ||
452 | getInputValue: (el) -> | ||
453 | if window.jQuery? | ||
454 | el = jQuery el | ||
455 | |||
456 | switch el[0].type | ||
457 | when 'checkbox' then el.is ':checked' | ||
458 | else el.val() | ||
459 | else | ||
460 | switch el.type | ||
461 | when 'checkbox' then el.checked | ||
462 | when 'select-multiple' then o.value for o in el when o.selected | ||
463 | else el.value | ||
464 | |||
465 | # Rivets.binders | ||
466 | # -------------- | ||
467 | |||
468 | # Core binders that are included with Rivets.js, publicly available on | ||
469 | # `module.binders`. Can be overridden globally or local to a `Rivets.View` | ||
470 | # instance. | ||
471 | Rivets.binders = | ||
472 | enabled: (el, value) -> | ||
473 | el.disabled = !value | ||
474 | |||
475 | disabled: (el, value) -> | ||
476 | el.disabled = !!value | ||
477 | |||
478 | checked: | ||
479 | publishes: true | ||
480 | bind: (el) -> | ||
481 | Rivets.Util.bindEvent el, 'change', @publish | ||
482 | unbind: (el) -> | ||
483 | Rivets.Util.unbindEvent el, 'change', @publish | ||
484 | routine: (el, value) -> | ||
485 | if el.type is 'radio' | ||
486 | el.checked = el.value?.toString() is value?.toString() | ||
487 | else | ||
488 | el.checked = !!value | ||
489 | |||
490 | unchecked: | ||
491 | publishes: true | ||
492 | bind: (el) -> | ||
493 | Rivets.Util.bindEvent el, 'change', @publish | ||
494 | unbind: (el) -> | ||
495 | Rivets.Util.unbindEvent el, 'change', @publish | ||
496 | routine: (el, value) -> | ||
497 | if el.type is 'radio' | ||
498 | el.checked = el.value?.toString() isnt value?.toString() | ||
499 | else | ||
500 | el.checked = !value | ||
501 | |||
502 | show: (el, value) -> | ||
503 | el.style.display = if value then '' else 'none' | ||
504 | |||
505 | hide: (el, value) -> | ||
506 | el.style.display = if value then 'none' else '' | ||
507 | |||
508 | html: (el, value) -> | ||
509 | el.innerHTML = if value? then value else '' | ||
510 | |||
511 | value: | ||
512 | publishes: true | ||
513 | bind: (el) -> | ||
514 | Rivets.Util.bindEvent el, 'change', @publish | ||
515 | unbind: (el) -> | ||
516 | Rivets.Util.unbindEvent el, 'change', @publish | ||
517 | routine: (el, value) -> | ||
518 | if window.jQuery? | ||
519 | el = jQuery el | ||
520 | |||
521 | if value?.toString() isnt el.val()?.toString() | ||
522 | el.val if value? then value else '' | ||
523 | else | ||
524 | if el.type is 'select-multiple' | ||
525 | o.selected = o.value in value for o in el if value? | ||
526 | else if value?.toString() isnt el.value?.toString() | ||
527 | el.value = if value? then value else '' | ||
528 | |||
529 | text: (el, value) -> | ||
530 | if el.innerText? | ||
531 | el.innerText = if value? then value else '' | ||
532 | else | ||
533 | el.textContent = if value? then value else '' | ||
534 | |||
535 | if: | ||
536 | block: true | ||
537 | |||
538 | bind: (el) -> | ||
539 | unless @marker? | ||
540 | attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-' | ||
541 | declaration = el.getAttribute attr | ||
542 | |||
543 | @marker = document.createComment " rivets: #{@type} #{declaration} " | ||
544 | |||
545 | el.removeAttribute attr | ||
546 | el.parentNode.insertBefore @marker, el | ||
547 | el.parentNode.removeChild el | ||
548 | |||
549 | unbind: -> | ||
550 | @nested?.unbind() | ||
551 | |||
552 | routine: (el, value) -> | ||
553 | if !!value is not @nested? | ||
554 | if value | ||
555 | models = {} | ||
556 | models[key] = model for key, model of @view.models | ||
557 | |||
558 | options = | ||
559 | binders: @view.options.binders | ||
560 | formatters: @view.options.formatters | ||
561 | adapters: @view.options.adapters | ||
562 | config: @view.options.config | ||
563 | |||
564 | (@nested = new Rivets.View(el, models, options)).bind() | ||
565 | @marker.parentNode.insertBefore el, @marker.nextSibling | ||
566 | else | ||
567 | el.parentNode.removeChild el | ||
568 | @nested.unbind() | ||
569 | delete @nested | ||
570 | |||
571 | update: (models) -> | ||
572 | @nested?.update models | ||
573 | |||
574 | unless: | ||
575 | block: true | ||
576 | |||
577 | bind: (el) -> | ||
578 | Rivets.binders.if.bind.call @, el | ||
579 | |||
580 | unbind: -> | ||
581 | Rivets.binders.if.unbind.call @ | ||
582 | |||
583 | routine: (el, value) -> | ||
584 | Rivets.binders.if.routine.call @, el, not value | ||
585 | |||
586 | update: (models) -> | ||
587 | Rivets.binders.if.update.call @, models | ||
588 | |||
589 | "on-*": | ||
590 | function: true | ||
591 | |||
592 | unbind: (el) -> | ||
593 | Rivets.Util.unbindEvent el, @args[0], @handler if @handler | ||
594 | |||
595 | routine: (el, value) -> | ||
596 | Rivets.Util.unbindEvent el, @args[0], @handler if @handler | ||
597 | Rivets.Util.bindEvent el, @args[0], @handler = @eventHandler value | ||
598 | |||
599 | "each-*": | ||
600 | block: true | ||
601 | |||
602 | bind: (el) -> | ||
603 | unless @marker? | ||
604 | attr = ['data', @view.config.prefix, @type].join('-').replace '--', '-' | ||
605 | @marker = document.createComment " rivets: #{@type} " | ||
606 | @iterated = [] | ||
607 | |||
608 | el.removeAttribute attr | ||
609 | el.parentNode.insertBefore @marker, el | ||
610 | el.parentNode.removeChild el | ||
611 | |||
612 | unbind: (el) -> | ||
613 | view.unbind() for view in @iterated if @iterated? | ||
614 | |||
615 | routine: (el, collection) -> | ||
616 | modelName = @args[0] | ||
617 | collection = collection or [] | ||
618 | |||
619 | if @iterated.length > collection.length | ||
620 | for i in Array @iterated.length - collection.length | ||
621 | view = @iterated.pop() | ||
622 | view.unbind() | ||
623 | @marker.parentNode.removeChild view.els[0] | ||
624 | |||
625 | for model, index in collection | ||
626 | data = {} | ||
627 | data[modelName] = model | ||
628 | |||
629 | if not @iterated[index]? | ||
630 | for key, model of @view.models | ||
631 | data[key] ?= model | ||
632 | |||
633 | previous = if @iterated.length | ||
634 | @iterated[@iterated.length - 1].els[0] | ||
635 | else | ||
636 | @marker | ||
637 | |||
638 | options = | ||
639 | binders: @view.options.binders | ||
640 | formatters: @view.options.formatters | ||
641 | adapters: @view.options.adapters | ||
642 | config: {} | ||
643 | |||
644 | options.config[k] = v for k, v of @view.options.config | ||
645 | options.config.preloadData = true | ||
646 | |||
647 | template = el.cloneNode true | ||
648 | view = new Rivets.View(template, data, options) | ||
649 | view.bind() | ||
650 | @iterated.push view | ||
651 | |||
652 | @marker.parentNode.insertBefore template, previous.nextSibling | ||
653 | else if @iterated[index].models[modelName] isnt model | ||
654 | @iterated[index].update data | ||
655 | |||
656 | update: (models) -> | ||
657 | data = {} | ||
658 | |||
659 | for key, model of models | ||
660 | data[key] = model unless key is @args[0] | ||
661 | |||
662 | view.update data for view in @iterated | ||
663 | |||
664 | "class-*": (el, value) -> | ||
665 | elClass = " #{el.className} " | ||
666 | |||
667 | if !value is (elClass.indexOf(" #{@args[0]} ") isnt -1) | ||
668 | el.className = if value | ||
669 | "#{el.className} #{@args[0]}" | ||
670 | else | ||
671 | elClass.replace(" #{@args[0]} ", ' ').trim() | ||
672 | |||
673 | "*": (el, value) -> | ||
674 | if value | ||
675 | el.setAttribute @type, value | ||
676 | else | ||
677 | el.removeAttribute @type | ||
678 | |||
679 | # Rivets.components | ||
680 | # ----------------- | ||
681 | |||
682 | # Default components (there aren't any), publicly accessible on | ||
683 | # `module.components`. Can be overridden globally or local to a `Rivets.View` | ||
684 | # instance. | ||
685 | Rivets.components = {} | ||
686 | |||
687 | # Rivets.formatters | ||
688 | # ----------------- | ||
689 | |||
690 | # Default formatters (there aren't any), publicly accessible on | ||
691 | # `module.formatters`. Can be overridden globally or local to a `Rivets.View` | ||
692 | # instance. | ||
693 | Rivets.formatters = {} | ||
694 | |||
695 | # Rivets.adapters | ||
696 | # ----------------- | ||
697 | |||
698 | # Default adapters (`.` for POJSO in ES5 natives), publicly accessible on | ||
699 | # `module.adapters`. Can be overridden globally or local to a `Rivets.View` | ||
700 | # instance. | ||
701 | Rivets.adapters = | ||
702 | '.': | ||
703 | id: '_rv' | ||
704 | counter: 0 | ||
705 | weakmap: {} | ||
706 | |||
707 | subscribe: (obj, keypath, callback) -> | ||
708 | unless obj[@id]? | ||
709 | obj[@id] = @counter++ | ||
710 | @weakmap[obj[@id]] = {} | ||
711 | |||
712 | map = @weakmap[obj[@id]] | ||
713 | |||
714 | unless map[keypath]? | ||
715 | map[keypath] = [] | ||
716 | value = obj[keypath] | ||
717 | |||
718 | Object.defineProperty obj, keypath, | ||
719 | get: -> value | ||
720 | set: (newValue) -> | ||
721 | if newValue isnt value | ||
722 | value = newValue | ||
723 | callback() for callback in map[keypath] | ||
724 | |||
725 | unless callback in map[keypath] | ||
726 | map[keypath].push callback | ||
727 | |||
728 | unsubscribe: (obj, keypath, callback) -> | ||
729 | callbacks = @weakmap[obj[@id]][keypath] | ||
730 | callbacks.splice callbacks.indexOf(callback), 1 | ||
731 | |||
732 | read: (obj, keypath) -> | ||
733 | obj[keypath] | ||
734 | |||
735 | publish: (obj, keypath, value) -> | ||
736 | obj[keypath] = value | ||
737 | |||
738 | # Rivets.config | ||
739 | # ------------- | ||
740 | |||
741 | # Default configuration, publicly accessible on `module.config`. Can be | ||
742 | # overridden globally or local to a `Rivets.View` instance. | ||
743 | Rivets.config = | ||
744 | preloadData: true | 22 | preloadData: true |
745 | handler: (context, ev, binding) -> | 23 | handler: (context, ev, binding) -> |
746 | @call context, ev, binding.view.models | 24 | @call context, ev, binding.view.models |
747 | |||
748 | # Rivets.factory | ||
749 | # -------------- | ||
750 | |||
751 | # The Rivets.js module factory. | ||
752 | Rivets.factory = (exports) -> | ||
753 | # Exposes the full Rivets namespace. This is mainly used for isolated testing. | ||
754 | exports._ = Rivets | ||
755 | |||
756 | # Exposes the core binding routines that can be extended or stripped down. | ||
757 | exports.binders = Rivets.binders | ||
758 | |||
759 | # Exposes the components object to be extended. | ||
760 | exports.components = Rivets.components | ||
761 | |||
762 | # Exposes the formatters object to be extended. | ||
763 | exports.formatters = Rivets.formatters | ||
764 | |||
765 | # Exposes the adapters object to be extended. | ||
766 | exports.adapters = Rivets.adapters | ||
767 | |||
768 | # Exposes the rivets configuration options. These can be set manually or from | ||
769 | # rivets.configure with an object literal. | ||
770 | exports.config = Rivets.config | ||
771 | |||
772 | # Sets configuration options by merging an object literal. | ||
773 | exports.configure = (options={}) -> | ||
774 | for property, value of options | ||
775 | Rivets.config[property] = value | ||
776 | return | ||
777 | |||
778 | # Binds a set of model objects to a parent DOM element and returns a | ||
779 | # `Rivets.View` instance. | ||
780 | exports.bind = (el, models = {}, options = {}) -> | ||
781 | view = new Rivets.View(el, models, options) | ||
782 | view.bind() | ||
783 | view | ||
784 | |||
785 | # Export | ||
786 | # ------ | ||
787 | |||
788 | # Exports Rivets.js for CommonJS, AMD and the browser. | ||
789 | if typeof exports == 'object' | ||
790 | Rivets.factory(exports) | ||
791 | else if typeof define == 'function' && define.amd | ||
792 | define ['exports'], (exports) -> | ||
793 | Rivets.factory(@rivets = exports) | ||
794 | return exports | ||
795 | else | ||
796 | Rivets.factory(@rivets = {}) | ... | ... |
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