spine-rails 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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