fd7fcbfa by Michael Richards

Separate classes and the adapter / binder definitions into their own files.

1 parent 5988bc88
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
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
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
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 = {})
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 = {})
......
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
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