spine-rails 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ Spine = @Spine or require('spine')
2
+ $ = Spine.$
3
+ Model = Spine.Model
4
+ Queue = $({})
5
+
6
+ Ajax =
7
+ getURL: (object) ->
8
+ object and object.url?() or object.url
9
+
10
+ getScope: (object) ->
11
+ scope = object and object.scope?() or object.scope
12
+ if scope? and scope.charAt(0) is '/'
13
+ scope = scope.substring(1)
14
+ scope
15
+
16
+ generateURL: (object, args...) ->
17
+ if object.className
18
+ collection = object.className.toLowerCase() + 's'
19
+ scope = Ajax.getScope(object)
20
+ else
21
+ if typeof object.constructor.url is 'string'
22
+ collection = object.constructor.url
23
+ collection = collection.substring(1) if collection.charAt(0) is '/'
24
+ else
25
+ collection = object.constructor.className.toLowerCase() + 's'
26
+ scope = Ajax.getScope(object) or Ajax.getScope(object.constructor)
27
+ args.unshift(collection)
28
+ if scope?
29
+ args.unshift(scope)
30
+ args.unshift(Model.host)
31
+ args.join('/')
32
+
33
+ enabled: true
34
+
35
+ disable: (callback) ->
36
+ if @enabled
37
+ @enabled = false
38
+ try
39
+ do callback
40
+ catch e
41
+ throw e
42
+ finally
43
+ @enabled = true
44
+ else
45
+ do callback
46
+
47
+ queue: (request) ->
48
+ if request then Queue.queue(request) else Queue.queue()
49
+
50
+ clearQueue: ->
51
+ @queue []
52
+
53
+ class Base
54
+ defaults:
55
+ dataType: 'json'
56
+ processData: false
57
+ headers: {'X-Requested-With': 'XMLHttpRequest'}
58
+
59
+ queue: Ajax.queue
60
+
61
+ ajax: (params, defaults) ->
62
+ $.ajax @ajaxSettings(params, defaults)
63
+
64
+ ajaxQueue: (params, defaults) ->
65
+ jqXHR = null
66
+ deferred = $.Deferred()
67
+ promise = deferred.promise()
68
+ return promise unless Ajax.enabled
69
+ settings = @ajaxSettings(params, defaults)
70
+
71
+ request = (next) ->
72
+ jqXHR = $.ajax(settings)
73
+ .done(deferred.resolve)
74
+ .fail(deferred.reject)
75
+ .then(next, next)
76
+
77
+ promise.abort = (statusText) ->
78
+ return jqXHR.abort(statusText) if jqXHR
79
+ index = $.inArray(request, @queue())
80
+ @queue().splice(index, 1) if index > -1
81
+ deferred.rejectWith(
82
+ settings.context or settings,
83
+ [promise, statusText, '']
84
+ )
85
+ promise
86
+
87
+ @queue request
88
+ promise
89
+
90
+ ajaxSettings: (params, defaults) ->
91
+ $.extend({}, @defaults, defaults, params)
92
+
93
+ class Collection extends Base
94
+ constructor: (@model) ->
95
+
96
+ find: (id, params) ->
97
+ record = new @model(id: id)
98
+ @ajaxQueue(
99
+ params,
100
+ type: 'GET',
101
+ url: Ajax.getURL(record)
102
+ ).done(@recordsResponse)
103
+ .fail(@failResponse)
104
+
105
+ all: (params) ->
106
+ @ajaxQueue(
107
+ params,
108
+ type: 'GET',
109
+ url: Ajax.getURL(@model)
110
+ ).done(@recordsResponse)
111
+ .fail(@failResponse)
112
+
113
+ fetch: (params = {}, options = {}) ->
114
+ if id = params.id
115
+ delete params.id
116
+ @find(id, params).done (record) =>
117
+ @model.refresh(record, options)
118
+ else
119
+ @all(params).done (records) =>
120
+ @model.refresh(records, options)
121
+
122
+ # Private
123
+
124
+ recordsResponse: (data, status, xhr) =>
125
+ @model.trigger('ajaxSuccess', null, status, xhr)
126
+
127
+ failResponse: (xhr, statusText, error) =>
128
+ @model.trigger('ajaxError', null, xhr, statusText, error)
129
+
130
+ class Singleton extends Base
131
+ constructor: (@record) ->
132
+ @model = @record.constructor
133
+
134
+ reload: (params, options) ->
135
+ @ajaxQueue(
136
+ params,
137
+ type: 'GET'
138
+ url: Ajax.getURL(@record)
139
+ ).done(@recordResponse(options))
140
+ .fail(@failResponse(options))
141
+
142
+ create: (params, options) ->
143
+ @ajaxQueue(
144
+ params,
145
+ type: 'POST'
146
+ contentType: 'application/json'
147
+ data: JSON.stringify(@record)
148
+ url: Ajax.getURL(@model)
149
+ ).done(@recordResponse(options))
150
+ .fail(@failResponse(options))
151
+
152
+ update: (params, options) ->
153
+ @ajaxQueue(
154
+ params,
155
+ type: 'PUT'
156
+ contentType: 'application/json'
157
+ data: JSON.stringify(@record)
158
+ url: Ajax.getURL(@record)
159
+ ).done(@recordResponse(options))
160
+ .fail(@failResponse(options))
161
+
162
+ destroy: (params, options) ->
163
+ @ajaxQueue(
164
+ params,
165
+ type: 'DELETE'
166
+ url: Ajax.getURL(@record)
167
+ ).done(@recordResponse(options))
168
+ .fail(@failResponse(options))
169
+
170
+ # Private
171
+
172
+ recordResponse: (options = {}) =>
173
+ (data, status, xhr) =>
174
+ if Spine.isBlank(data) or @record.destroyed
175
+ data = false
176
+ else
177
+ data = @model.fromJSON(data)
178
+
179
+ Ajax.disable =>
180
+ if data
181
+ # ID change, need to do some shifting
182
+ if data.id and @record.id isnt data.id
183
+ @record.changeID(data.id)
184
+ # Update with latest data
185
+ @record.updateAttributes(data.attributes())
186
+
187
+ @record.trigger('ajaxSuccess', data, status, xhr)
188
+ options.success?.apply(@record) # Deprecated
189
+ options.done?.apply(@record)
190
+
191
+ failResponse: (options = {}) =>
192
+ (xhr, statusText, error) =>
193
+ @record.trigger('ajaxError', xhr, statusText, error)
194
+ options.error?.apply(@record) # Deprecated
195
+ options.fail?.apply(@record)
196
+
197
+ # Ajax endpoint
198
+ Model.host = ''
199
+
200
+ Include =
201
+ ajax: -> new Singleton(this)
202
+
203
+ url: (args...) ->
204
+ args.unshift(encodeURIComponent(@id))
205
+ Ajax.generateURL(@, args...)
206
+
207
+ Extend =
208
+ ajax: -> new Collection(this)
209
+
210
+ url: (args...) ->
211
+ Ajax.generateURL(@, args...)
212
+
213
+ Model.Ajax =
214
+ extended: ->
215
+ @fetch @ajaxFetch
216
+ @change @ajaxChange
217
+ @extend Extend
218
+ @include Include
219
+
220
+ # Private
221
+
222
+ ajaxFetch: ->
223
+ @ajax().fetch(arguments...)
224
+
225
+ ajaxChange: (record, type, options = {}) ->
226
+ return if options.ajax is false
227
+ record.ajax()[type](options.ajax, options)
228
+
229
+ Model.Ajax.Methods =
230
+ extended: ->
231
+ @extend Extend
232
+ @include Include
233
+
234
+ # Globals
235
+ Ajax.defaults = Base::defaults
236
+ Spine.Ajax = Ajax
237
+ module?.exports = Ajax
@@ -0,0 +1,43 @@
1
+ Spine = @Spine or require('spine')
2
+ $ = Spine.$
3
+
4
+ class Spine.List extends Spine.Controller
5
+ events:
6
+ 'click .item': 'click'
7
+
8
+ selectFirst: false
9
+
10
+ constructor: ->
11
+ super
12
+ @bind 'change', @change
13
+
14
+ template: ->
15
+ throw 'Override template'
16
+
17
+ change: (item) =>
18
+ @current = item
19
+
20
+ unless @current
21
+ @children().removeClass('active')
22
+ return
23
+
24
+ @children().removeClass('active')
25
+ $(@children().get(@items.indexOf(@current))).addClass('active')
26
+
27
+ render: (items) ->
28
+ @items = items if items
29
+ @html @template(@items)
30
+ @change @current
31
+ if @selectFirst
32
+ unless @children('.active').length
33
+ @children(':first').click()
34
+
35
+ children: (sel) ->
36
+ @el.children(sel)
37
+
38
+ click: (e) ->
39
+ item = @items[$(e.currentTarget).index()]
40
+ @trigger('change', item)
41
+ true
42
+
43
+ module?.exports = Spine.List
@@ -0,0 +1,16 @@
1
+ Spine = @Spine or require('spine')
2
+
3
+ Spine.Model.Local =
4
+ extended: ->
5
+ @change @saveLocal
6
+ @fetch @loadLocal
7
+
8
+ saveLocal: ->
9
+ result = JSON.stringify(@)
10
+ localStorage[@className] = result
11
+
12
+ loadLocal: ->
13
+ result = localStorage[@className]
14
+ @refresh(result or [], clear: true)
15
+
16
+ module?.exports = Spine.Model.Local
@@ -0,0 +1,84 @@
1
+ Spine = @Spine or require('spine')
2
+ $ = Spine.$
3
+
4
+ class Spine.Manager extends Spine.Module
5
+ @include Spine.Events
6
+
7
+ constructor: ->
8
+ @controllers = []
9
+ @bind 'change', @change
10
+ @add(arguments...)
11
+
12
+ add: (controllers...) ->
13
+ @addOne(cont) for cont in controllers
14
+
15
+ addOne: (controller) ->
16
+ controller.bind 'active', (args...) =>
17
+ @trigger('change', controller, args...)
18
+ controller.bind 'release', =>
19
+ @controllers.splice(@controllers.indexOf(controller), 1)
20
+
21
+ @controllers.push(controller)
22
+
23
+ deactivate: ->
24
+ @trigger('change', false, arguments...)
25
+
26
+ # Private
27
+
28
+ change: (current, args...) ->
29
+ for cont in @controllers
30
+ if cont is current
31
+ cont.activate(args...)
32
+ else
33
+ cont.deactivate(args...)
34
+
35
+ Spine.Controller.include
36
+ active: (args...) ->
37
+ if typeof args[0] is 'function'
38
+ @bind('active', args[0])
39
+ else
40
+ args.unshift('active')
41
+ @trigger(args...)
42
+ @
43
+
44
+ isActive: ->
45
+ @el.hasClass('active')
46
+
47
+ activate: ->
48
+ @el.addClass('active')
49
+ @
50
+
51
+ deactivate: ->
52
+ @el.removeClass('active')
53
+ @
54
+
55
+ class Spine.Stack extends Spine.Controller
56
+ controllers: {}
57
+ routes: {}
58
+
59
+ className: 'spine stack'
60
+
61
+ constructor: ->
62
+ super
63
+
64
+ @manager = new Spine.Manager
65
+
66
+ for key, value of @controllers
67
+ throw Error "'@#{ key }' already assigned - choose a different name" if @[key]?
68
+ @[key] = new value(stack: @)
69
+ @add(@[key])
70
+
71
+ for key, value of @routes
72
+ do (key, value) =>
73
+ callback = value if typeof value is 'function'
74
+ callback or= => @[value].active(arguments...)
75
+ @route(key, callback)
76
+
77
+ @[@default].active() if @default
78
+
79
+ add: (controller) ->
80
+ @manager.add(controller)
81
+ @append(controller)
82
+
83
+ module?.exports = Spine.Manager
84
+ module?.exports.Stack = Spine.Stack
@@ -0,0 +1,136 @@
1
+ Spine = @Spine or require('spine')
2
+ isArray = Spine.isArray
3
+ require = @require or ((value) -> eval(value))
4
+
5
+ class Collection extends Spine.Module
6
+ constructor: (options = {}) ->
7
+ for key, value of options
8
+ @[key] = value
9
+
10
+ all: ->
11
+ @model.select (rec) => @associated(rec)
12
+
13
+ first: ->
14
+ @all()[0]
15
+
16
+ last: ->
17
+ values = @all()
18
+ values[values.length - 1]
19
+
20
+ count: ->
21
+ @all().length
22
+
23
+ find: (id) ->
24
+ records = @select (rec) =>
25
+ "#{rec.id}" is "#{id}"
26
+ throw new Error("\"#{@model.className}\" model could not find a record for the ID \"#{id}\"") unless records[0]
27
+ records[0]
28
+
29
+ findAllByAttribute: (name, value) ->
30
+ @model.select (rec) =>
31
+ @associated(rec) and rec[name] is value
32
+
33
+ findByAttribute: (name, value) ->
34
+ @findAllByAttribute(name, value)[0]
35
+
36
+ select: (cb) ->
37
+ @model.select (rec) =>
38
+ @associated(rec) and cb(rec)
39
+
40
+ refresh: (values) ->
41
+ return this unless values?
42
+ for record in @all()
43
+ delete @model.irecords[record.id]
44
+ for match, i in @model.records when match.id is record.id
45
+ @model.records.splice(i, 1)
46
+ break
47
+ values = [values] unless isArray(values)
48
+ for record in values
49
+ record.newRecord = false
50
+ record[@fkey] = @record.id
51
+ @model.refresh values
52
+ this
53
+
54
+ create: (record, options) ->
55
+ record[@fkey] = @record.id
56
+ @model.create(record, options)
57
+
58
+ add: (record, options) ->
59
+ record.updateAttribute @fkey, @record.id, options
60
+
61
+ remove: (record, options) ->
62
+ record.updateAttribute @fkey, null, options
63
+
64
+ # Private
65
+
66
+ associated: (record) ->
67
+ record[@fkey] is @record.id
68
+
69
+ class Instance extends Spine.Module
70
+ constructor: (options = {}) ->
71
+ for key, value of options
72
+ @[key] = value
73
+
74
+ exists: ->
75
+ return if @record[@fkey] then @model.exists(@record[@fkey]) else false
76
+
77
+ update: (value) ->
78
+ return this unless value?
79
+ unless value instanceof @model
80
+ value = new @model(value)
81
+ value.save() if value.isNew()
82
+ @record[@fkey] = value and value.id
83
+ this
84
+
85
+ class Singleton extends Spine.Module
86
+ constructor: (options = {}) ->
87
+ for key, value of options
88
+ @[key] = value
89
+
90
+ find: ->
91
+ @record.id and @model.findByAttribute(@fkey, @record.id)
92
+
93
+ update: (value) ->
94
+ return this unless value?
95
+ unless value instanceof @model
96
+ value = @model.fromJSON(value)
97
+
98
+ value[@fkey] = @record.id
99
+ value.save()
100
+ this
101
+
102
+ singularize = (str) ->
103
+ str.replace(/s$/, '')
104
+
105
+ underscore = (str) ->
106
+ str.replace(/::/g, '/')
107
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
108
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
109
+ .replace(/-/g, '_')
110
+ .toLowerCase()
111
+
112
+ association = (name, model, record, fkey, Ctor) ->
113
+ model = require(model) if typeof model is 'string'
114
+ new Ctor(name: name, model: model, record: record, fkey: fkey)
115
+
116
+ Spine.Model.extend
117
+ hasMany: (name, model, fkey) ->
118
+ fkey ?= "#{underscore(this.className)}_id"
119
+ @::[name] = (value) ->
120
+ association(name, model, @, fkey, Collection).refresh(value)
121
+
122
+ belongsTo: (name, model, fkey) ->
123
+ fkey ?= "#{underscore(singularize(name))}_id"
124
+ @::[name] = (value) ->
125
+ association(name, model, @, fkey, Instance).update(value).exists()
126
+
127
+ @attributes.push(fkey)
128
+
129
+ hasOne: (name, model, fkey) ->
130
+ fkey ?= "#{underscore(@className)}_id"
131
+ @::[name] = (value) ->
132
+ association(name, model, @, fkey, Singleton).update(value).find()
133
+
134
+ Spine.Collection = Collection
135
+ Spine.Singleton = Singleton
136
+ Spine.Instance = Instance