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.
- data/README.md +12 -3
- data/build/update.sh +9 -0
- data/lib/generators/spine/model/model_generator.rb +1 -1
- data/lib/generators/spine/new/new_generator.rb +14 -14
- data/lib/generators/spine/scaffold/scaffold_generator.rb +5 -5
- data/lib/generators/spine/scaffold/templates/controller.coffee.erb +1 -1
- data/lib/spine/generators.rb +5 -5
- data/lib/spine/rails/version.rb +2 -2
- data/spine-rails.gemspec +2 -1
- data/vendor/assets/javascripts/spine.coffee +640 -0
- data/vendor/assets/javascripts/spine/ajax.coffee +237 -0
- data/vendor/assets/javascripts/spine/list.coffee +43 -0
- data/vendor/assets/javascripts/spine/local.coffee +16 -0
- data/vendor/assets/javascripts/spine/manager.coffee +84 -0
- data/vendor/assets/javascripts/spine/relation.coffee +136 -0
- data/vendor/assets/javascripts/spine/route.coffee +148 -0
- metadata +41 -17
- data/app/assets/javascripts/json2.js +0 -485
- data/app/assets/javascripts/spine.js +0 -808
- data/app/assets/javascripts/spine/ajax.js +0 -290
- data/app/assets/javascripts/spine/list.js +0 -70
- data/app/assets/javascripts/spine/local.js +0 -28
- data/app/assets/javascripts/spine/manager.js +0 -154
- data/app/assets/javascripts/spine/relation.js +0 -222
- data/app/assets/javascripts/spine/route.js +0 -197
- data/app/assets/javascripts/spine/tabs.js +0 -66
- data/app/assets/javascripts/spine/tmpl.js +0 -22
@@ -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
|