live_record 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +11 -0
- data/README.md +369 -0
- data/app/assets/javascripts/live_record.js +8 -0
- data/app/assets/javascripts/live_record/helpers.js +4 -0
- data/app/assets/javascripts/live_record/helpers/load_records.coffee +31 -0
- data/app/assets/javascripts/live_record/model.js +4 -0
- data/app/assets/javascripts/live_record/model/all.coffee +1 -0
- data/app/assets/javascripts/live_record/model/create.coffee +192 -0
- data/app/assets/javascripts/live_record/plugins.js +3 -0
- data/app/assets/javascripts/live_record/plugins/live_dom.coffee +18 -0
- data/lib/live_record.rb +8 -0
- data/lib/live_record/channel.rb +99 -0
- data/lib/live_record/engine.rb +4 -0
- data/lib/live_record/generators/install_generator.rb +72 -0
- data/lib/live_record/generators/templates/create_live_record_updates.rb +8 -0
- data/lib/live_record/generators/templates/index.html.erb +34 -0
- data/lib/live_record/generators/templates/javascript.coffee.rb +16 -0
- data/lib/live_record/generators/templates/javascript.js.rb +16 -0
- data/lib/live_record/generators/templates/live_record_channel.rb +4 -0
- data/lib/live_record/generators/templates/live_record_update.rb +3 -0
- data/lib/live_record/generators/templates/model.rb.rb +19 -0
- data/lib/live_record/generators/templates/show.html.erb +16 -0
- data/lib/live_record/model.rb +36 -0
- data/lib/live_record/version.rb +3 -0
- data/live_record.gemspec +20 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: '00451863c5ada349c3b74b4c4ad8fd6332e748fe'
|
4
|
+
data.tar.gz: ed18de46bdb44a11f5d54fa7ff76af65202381d9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 064c14f91640de8fb7ef723fc147c7ee475d4c2223469b1eb4fa0c2c0131e95a9b2df308f195f688bd833bdf7dd110807f15d7cceb71a6a355d87da53edf6d72
|
7
|
+
data.tar.gz: 0a4f1d3fc4312cd88757c2f4dbb434944bd7cc52d2f665d0a8a5e3d4bf58296030cae6fe6334ca31daf1005cc3e0f43dae64b1eac92f924638ecbcc4829d5bd7
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -0,0 +1,369 @@
|
|
1
|
+
*WIP! (TODO: Tests)*
|
2
|
+
|
3
|
+
## About
|
4
|
+
|
5
|
+
* Auto-syncs records in client-side JS (through a Model DSL) from changes in the backend Rails server through ActionCable
|
6
|
+
* Auto-updates DOM elements mapped to a record attribute, from changes. **(Optional Plugin)**
|
7
|
+
* Automatically resyncs after client-side reconnection.
|
8
|
+
|
9
|
+
> `live_record` is intentionally designed for read-only one-way syncing from the backend server, and does not support pushing changes to the Rails server from the client-side JS. Updates from client-side then is intended to use the normal HTTP REST requests.
|
10
|
+
|
11
|
+
## Requirements
|
12
|
+
|
13
|
+
* **>= Rails 5.0**
|
14
|
+
|
15
|
+
## Demo
|
16
|
+
|
17
|
+
* https://obscure-refuge-63797.herokuapp.com/
|
18
|
+
|
19
|
+
## Usage Example
|
20
|
+
|
21
|
+
* on the client-side:
|
22
|
+
|
23
|
+
```js
|
24
|
+
// instantiate a Book object
|
25
|
+
var book = new LiveRecord.Model.all.Book({
|
26
|
+
id: 1,
|
27
|
+
title: 'Harry Potter',
|
28
|
+
author: 'J. K. Rowling',
|
29
|
+
created_at: '2017-08-02T12:39:49.238Z',
|
30
|
+
updated_at: '2017-08-02T12:39:49.238Z'
|
31
|
+
});
|
32
|
+
// store this Book object into the JS store
|
33
|
+
book.create();
|
34
|
+
|
35
|
+
// the store is accessible through
|
36
|
+
LiveRecord.Model.all.Book.all;
|
37
|
+
|
38
|
+
// all records in the JS store are automatically subscribed to the backend LiveRecordChannel, which meant syncing (update / destroy) changes from the backend
|
39
|
+
|
40
|
+
// you can add a callback that will be invoked whenever the Book object has been updated
|
41
|
+
book.addCallback('after:update', function() {
|
42
|
+
// let's say you update the DOM elements here when the attributes have changed
|
43
|
+
});
|
44
|
+
```
|
45
|
+
|
46
|
+
* on the backend-side, you can handle attributes authorisation:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# app/models/book.rb
|
50
|
+
class Book < ApplicationRecord
|
51
|
+
def self.live_record_whitelisted_attributes(book, current_user)
|
52
|
+
# Add attributes to this array that you would like `current_user` to have access to when syncing this particular `book`
|
53
|
+
# empty array means not-authorised
|
54
|
+
if book.user == current_user
|
55
|
+
[:title, :author, :created_at, :updated_at, :reference_id, :origin_address]
|
56
|
+
elsif current_user.present?
|
57
|
+
[:title, :author, :created_at, :updated_at]
|
58
|
+
else
|
59
|
+
[]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
* whenever a Book (or any other Model record that you specified) has been updated / destroyed, there exists an `after_update_commit` and an `after_destroy_commit` ActiveRecord callback that will broadcast changes to all subscribed JS clients
|
66
|
+
|
67
|
+
## Setup
|
68
|
+
* Add the following to your `Gemfile`:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
gem 'live_record', '~> 0.0.1'
|
72
|
+
```
|
73
|
+
|
74
|
+
* Run:
|
75
|
+
|
76
|
+
```bash
|
77
|
+
bundle install
|
78
|
+
```
|
79
|
+
|
80
|
+
* Install by running:
|
81
|
+
|
82
|
+
```bash
|
83
|
+
rails generate live_record:install
|
84
|
+
```
|
85
|
+
|
86
|
+
> `rails generate live_record:install --live_dom=false` if you do not need the `LiveDom` plugin; `--live_dom=true` by default
|
87
|
+
|
88
|
+
* Run migration to create the `live_record_updates` table, which is going to be used for client reconnection resyncing:
|
89
|
+
|
90
|
+
```bash
|
91
|
+
rake db:migrate
|
92
|
+
```
|
93
|
+
|
94
|
+
* Update your **app/channels/application_cable/connection.rb**, and add `current_user` method, unless you already have it:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
module ApplicationCable
|
98
|
+
class Connection < ActionCable::Connection::Base
|
99
|
+
identified_by :current_user
|
100
|
+
|
101
|
+
def current_user
|
102
|
+
# write something here if you have a current_user, or you may just leave this blank. Example below when using `devise` gem:
|
103
|
+
# User.find_by(id: cookies.signed[:user_id])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
* Update your **model** files (only those you would want to be synced), and insert the following public method:
|
110
|
+
|
111
|
+
> automatically updated if you use Rails scaffold or model generator
|
112
|
+
|
113
|
+
### Example 1 - Simple Usage
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
# app/models/book.rb (example 1)
|
117
|
+
class Book < ApplicationRecord
|
118
|
+
def self.live_record_whitelisted_attributes(book, current_user)
|
119
|
+
# Add attributes to this array that you would like current_user to have access to when syncing.
|
120
|
+
# Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
|
121
|
+
[:title, :author, :created_at, :updated_at]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
### Example 2 - Advanced Usage
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
# app/models/book.rb (example 1)
|
130
|
+
class Book < ApplicationRecord
|
131
|
+
def self.live_record_whitelisted_attributes(book, current_user)
|
132
|
+
# Notice that from above, you also have access to `book` (the record currently requested by the client to be synced),
|
133
|
+
# and the `current_user`, the current user who is trying to sync the `book` record.
|
134
|
+
if book.user == current_user
|
135
|
+
[:title, :author, :created_at, :updated_at, :reference_id, :origin_address]
|
136
|
+
elsif current_user.present?
|
137
|
+
[:title, :author, :created_at, :updated_at]
|
138
|
+
else
|
139
|
+
[]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
* For each Model you want to sync, insert the following in your Javascript files.
|
146
|
+
|
147
|
+
> automatically updated if you use Rails scaffold or controller generator
|
148
|
+
|
149
|
+
### Example 1 - Model
|
150
|
+
|
151
|
+
```js
|
152
|
+
// app/assets/javascripts/books.js
|
153
|
+
LiveRecord.Model.create(
|
154
|
+
{
|
155
|
+
modelName: 'Book' // should match the Rails model name
|
156
|
+
plugins: {
|
157
|
+
LiveDOM: true // remove this if you do not need `LiveDom`
|
158
|
+
}
|
159
|
+
}
|
160
|
+
)
|
161
|
+
```
|
162
|
+
|
163
|
+
### Example 2 - Model + Callbacks
|
164
|
+
|
165
|
+
```js
|
166
|
+
// app/assets/javascripts/books.js
|
167
|
+
LiveRecord.Model.create(
|
168
|
+
{
|
169
|
+
modelName: 'Book',
|
170
|
+
callbacks: {
|
171
|
+
'on:connect': [
|
172
|
+
function() {
|
173
|
+
console.log(this); // `this` refers to the current `Book` record that has just connected for syncing
|
174
|
+
}
|
175
|
+
],
|
176
|
+
'after:update': [
|
177
|
+
function() {
|
178
|
+
console.log(this); // `this` refers to the current `Book` record that has just been updated with changes synced from the backend
|
179
|
+
}
|
180
|
+
]
|
181
|
+
}
|
182
|
+
}
|
183
|
+
)
|
184
|
+
```
|
185
|
+
|
186
|
+
#### Model Callbacks supported:
|
187
|
+
* `on:connect`
|
188
|
+
* `on:disconnect`
|
189
|
+
* `on:response_error`
|
190
|
+
* `before:create`
|
191
|
+
* `after:create`
|
192
|
+
* `before:update`
|
193
|
+
* `after:update`
|
194
|
+
* `before:destroy`
|
195
|
+
* `after:destroy`
|
196
|
+
|
197
|
+
> Each callback should map to an array of functions
|
198
|
+
|
199
|
+
* `on:response_error` supports a function argument: The "Error Code". i.e.
|
200
|
+
|
201
|
+
### Example 3 - Handling Response Error
|
202
|
+
|
203
|
+
```js
|
204
|
+
LiveRecord.Model.create(
|
205
|
+
{
|
206
|
+
modelName: 'Book',
|
207
|
+
callbacks: {
|
208
|
+
'on:response_error': [
|
209
|
+
function(errorCode) {
|
210
|
+
console.log(errorCode); // errorCode is a string, representing the type of error. See Response Error Codes below:
|
211
|
+
}
|
212
|
+
]
|
213
|
+
}
|
214
|
+
}
|
215
|
+
)
|
216
|
+
```
|
217
|
+
|
218
|
+
#### Response Error Codes:
|
219
|
+
* `"forbidden"` - Current User is not authorized to sync record changes. Happens when Model's `live_record_whitelisted_attributes` method returns empty array.
|
220
|
+
* `"bad_request"` - Happens when `LiveRecord.Model.create({modelName: 'INCORRECTMODELNAME'})`
|
221
|
+
|
222
|
+
* Load the records into the JS Model-store through JSON REST (i.e.):
|
223
|
+
|
224
|
+
### Example 1 - Using Default Loader (Requires JQuery)
|
225
|
+
|
226
|
+
> Your controller must also support responding with JSON in addition to HTML. If you used scaffold or controller generator, this should already work immediately.
|
227
|
+
|
228
|
+
```html
|
229
|
+
<!-- app/views/books/index.html.erb -->
|
230
|
+
<script>
|
231
|
+
// `loadRecords` asynchronously loads all records (using the current URL) to the store, through a JSON AJAX request.
|
232
|
+
// in this example, `loadRecords` will load JSON from the current URL which is /books
|
233
|
+
LiveRecord.helpers.loadRecords({modelName: 'Book'})
|
234
|
+
</script>
|
235
|
+
```
|
236
|
+
|
237
|
+
```html
|
238
|
+
<!-- app/views/posts/index.html.erb -->
|
239
|
+
<script>
|
240
|
+
// You may also pass in a callback for synchronous logic
|
241
|
+
LiveRecord.helpers.loadRecords({
|
242
|
+
modelName: 'Book',
|
243
|
+
onLoad: function(records) {
|
244
|
+
// ...
|
245
|
+
},
|
246
|
+
onError: function(jqxhr, textStatus, error) {
|
247
|
+
// ...
|
248
|
+
}
|
249
|
+
})
|
250
|
+
</script>
|
251
|
+
```
|
252
|
+
|
253
|
+
### Example 2 - Using Custom Loader
|
254
|
+
|
255
|
+
```js
|
256
|
+
// do something here that will fetch Book record attributes...
|
257
|
+
// as an example, say you already have the following attributes:
|
258
|
+
var book1Attributes = { id: 1, title: 'Noli Me Tangere', author: 'José Rizal' }
|
259
|
+
var book2Attributes = { id: 2, title: 'ABNKKBSNPLAko?!', author: 'Bob Ong' }
|
260
|
+
|
261
|
+
// then we instantiate a Book object
|
262
|
+
var book1 = new LiveRecord.Model.all.Book(book1Attributes);
|
263
|
+
// then we push this Book object to the Book store, which then automatically subscribes them to changes in the backend
|
264
|
+
book1.create();
|
265
|
+
|
266
|
+
var book2 = new LiveRecord.Model.all.Book(book2Attributes);
|
267
|
+
book2.create();
|
268
|
+
|
269
|
+
// you can also add Instance callbacks specific only to this Object (supported callbacks are the same as the Model callbacks)
|
270
|
+
book2.addCallback('after:update', function() {
|
271
|
+
// do something when book2 has been updated after syncing
|
272
|
+
})
|
273
|
+
```
|
274
|
+
|
275
|
+
|
276
|
+
## Plugins
|
277
|
+
|
278
|
+
### LiveDom (Requires JQuery)
|
279
|
+
|
280
|
+
* enabled by default, unless explicitly removed.
|
281
|
+
* `LiveDom` allows DOM elements' text content to be automatically updated, whenever the mapped record-attribute has been updated.
|
282
|
+
|
283
|
+
> text content is safely escaped using JQuery's `.text()` function
|
284
|
+
|
285
|
+
#### Example 1 (Mapping to a Record-Attribute: `after:update`)
|
286
|
+
```html
|
287
|
+
<span data-live-record-update-from='Book-24-title'>Harry Potter</span>
|
288
|
+
```
|
289
|
+
|
290
|
+
* `data-live-record-update-from` format should be `MODELNAME-RECORDID-RECORDATTRIBUTE`
|
291
|
+
* whenever `LiveRecord.all.Book.all[24]` has been updated/synced from backend, "Harry Potter" text above changes accordingly.
|
292
|
+
* this does not apply to only `<span>` elements. You can use whatever elements you like.
|
293
|
+
|
294
|
+
#### Example 2 (Mapping to a Record: `after:destroy`)
|
295
|
+
|
296
|
+
```html
|
297
|
+
<section data-live-record-destroy-from='Book-31'>This example element is a container for the Book-31 record which can also contain children elements</section>
|
298
|
+
```
|
299
|
+
|
300
|
+
* `data-live-record-destroy-from` format should be `MODELNAME-RECORDID`
|
301
|
+
* whenever `LiveRecord.all.Book.all[31]` has been destroyed/synced from backend, the `<section>` element above is removed, and thus all of its children elements.
|
302
|
+
* this does not apply to only `<section>` elements. You can use whatever elements you like.
|
303
|
+
|
304
|
+
* You may combine `data-live-record-destroy-from` and `data-live-record-update-from` within the same element.
|
305
|
+
|
306
|
+
## JS API
|
307
|
+
|
308
|
+
`LiveRecord.Model.create(CONFIG)`
|
309
|
+
* `CONFIG` (Object)
|
310
|
+
* `modelName`: (String, Required)
|
311
|
+
* `callbacks`: (Object)
|
312
|
+
* `on:connect`: (Array of functions)
|
313
|
+
* `on:disconnect`: (Array of functions)
|
314
|
+
* `on:response_error`: (Array of functions; function argument = ERROR_CODE (String))
|
315
|
+
* `before:create`: (Array of functions)
|
316
|
+
* `after:create`: (Array of functions)
|
317
|
+
* `before:update`: (Array of functions)
|
318
|
+
* `after:update`: (Array of functions)
|
319
|
+
* `before:destroy`: (Array of functions)
|
320
|
+
* `after:destroy`: (Array of functions)
|
321
|
+
* `plugins`: (Object)
|
322
|
+
* `LiveDom`: (Boolean)
|
323
|
+
* returns the newly create `MODEL`
|
324
|
+
|
325
|
+
`new LiveRecord.Model.all.MODELNAME(ATTRIBUTES)`
|
326
|
+
* `ATTRIBUTES` (Object)
|
327
|
+
* returns a `MODELINSTANCE` of the the Model having `ATTRIBUTES` attributes
|
328
|
+
|
329
|
+
`MODELINSTANCE.modelName()`
|
330
|
+
* returns the model name (i.e. 'Book')
|
331
|
+
|
332
|
+
`MODELINSTANCE.attributes`
|
333
|
+
* the attributes object
|
334
|
+
|
335
|
+
`MODELINSTANCE.ATTRIBUTENAME()`
|
336
|
+
* returns the attribute value of corresponding to `ATTRIBUTENAME`. (i.e. `bookInstance.id()`, `bookInstance.created_at()`)
|
337
|
+
|
338
|
+
`MODELINSTANCE.subscribe()`
|
339
|
+
* subscribes to the `LiveRecordChannel`. This instance should already be subscribed by default after being stored, unless there is a `on:response_error` or manually `unsubscribed()` which then you should manually call this `subscribe()` function after correctly handling the response error, or whenever desired.
|
340
|
+
* returns the `subscription` object (the ActionCable subscription object itself)
|
341
|
+
|
342
|
+
`MODELINSTANCE.isSubscribed()`
|
343
|
+
* returns `true` or `false` accordingly if the instance is subscribed
|
344
|
+
|
345
|
+
`MODELINSTANCE.subscription`
|
346
|
+
* the `subscription` object (the ActionCable subscription object itself)
|
347
|
+
|
348
|
+
`MODELINSTANCE.create()`
|
349
|
+
* stores the instance to the store, and then `subscribe()` to the `LiveRecordChannel` for syncing
|
350
|
+
* returns the instance
|
351
|
+
|
352
|
+
`MODELINSTANCE.update(ATTRIBUTES)`
|
353
|
+
* `ATTRIBUTES` (Object)
|
354
|
+
* updates the attributes of the instance
|
355
|
+
* returns the instance
|
356
|
+
|
357
|
+
`MODELINSTANCE.destroy()`
|
358
|
+
* removes the instance from the store, and then `unsubscribe()`
|
359
|
+
* returns the instance
|
360
|
+
|
361
|
+
`MODELINSTANCE.addCallback(CALLBACKKEY, CALLBACKFUNCTION)`
|
362
|
+
* `CALLBACKKEY` (String) see supported callbacks above
|
363
|
+
* `CALLBACKFUNCTION` (function Object)
|
364
|
+
* returns the function Object if successfuly added, else returns `false` if callback already added
|
365
|
+
|
366
|
+
`MODELINSTANCE.removeCallback(CALLBACKKEY, CALLBACKFUNCTION)`
|
367
|
+
* `CALLBACKKEY` (String) see supported callbacks above
|
368
|
+
* `CALLBACKFUNCTION` (function Object) the function callback that will be removed
|
369
|
+
* returns the function Object if successfully removed, else returns `false` if callback is already removed
|
@@ -0,0 +1,31 @@
|
|
1
|
+
LiveRecord.helpers.loadRecords = (args) ->
|
2
|
+
args['modelName'] || throw new Error(':modelName argument required')
|
3
|
+
throw new Error(':modelName is not defined in LiveRecord.Model.all') if LiveRecord.Model.all[args['modelName']] == undefined
|
4
|
+
|
5
|
+
args['url'] ||= window.location.href
|
6
|
+
|
7
|
+
$.getJSON(
|
8
|
+
args['url']
|
9
|
+
).done(
|
10
|
+
(data) ->
|
11
|
+
# Array JSON
|
12
|
+
if $.isArray(data)
|
13
|
+
records_attributes = data;
|
14
|
+
records = []
|
15
|
+
|
16
|
+
for record_attributes in records_attributes
|
17
|
+
record = new LiveRecord.Model.all[args['modelName']](record_attributes);
|
18
|
+
record.create();
|
19
|
+
records << record
|
20
|
+
|
21
|
+
# Single-Record JSON
|
22
|
+
else
|
23
|
+
record_attributes = data
|
24
|
+
record = new LiveRecord.Model.all[args['modelName']](record_attributes);
|
25
|
+
record.create();
|
26
|
+
|
27
|
+
args['onLoad'].call(this, records) if args['onLoad']
|
28
|
+
).fail(
|
29
|
+
(jqxhr, textStatus, error) ->
|
30
|
+
args['onError'].call(this, jqxhr, textStatus, error) if args['onError']
|
31
|
+
)
|
@@ -0,0 +1 @@
|
|
1
|
+
LiveRecord.Model.all = {}
|
@@ -0,0 +1,192 @@
|
|
1
|
+
LiveRecord.Model.create = (config) ->
|
2
|
+
config.modelName != undefined || throw new Error('missing :modelName argument')
|
3
|
+
config.callbacks != undefined || config.callbacks = {}
|
4
|
+
config.plugins != undefined || config.callbacks = {}
|
5
|
+
|
6
|
+
# NEW
|
7
|
+
Model = (attributes) ->
|
8
|
+
this.attributes = attributes
|
9
|
+
# instance callbacks
|
10
|
+
this._callbacks = {
|
11
|
+
'on:connect': [],
|
12
|
+
'on:disconnect': [],
|
13
|
+
'on:response_error': [],
|
14
|
+
'before:create': [],
|
15
|
+
'after:create': [],
|
16
|
+
'before:update': [],
|
17
|
+
'after:update': [],
|
18
|
+
'before:destroy': [],
|
19
|
+
'after:destroy': []
|
20
|
+
}
|
21
|
+
|
22
|
+
Object.keys(this.attributes).forEach (attribute_key) ->
|
23
|
+
if Model.prototype[attribute_key] == undefined
|
24
|
+
Model.prototype[attribute_key] = ->
|
25
|
+
this.attributes[attribute_key]
|
26
|
+
this
|
27
|
+
|
28
|
+
Model.modelName = config.modelName
|
29
|
+
|
30
|
+
Model.prototype.modelName = ->
|
31
|
+
Model.modelName
|
32
|
+
|
33
|
+
Model.prototype.subscribe = ->
|
34
|
+
return this.subscription if this.subscription != undefined
|
35
|
+
|
36
|
+
# listen for record changes (update / destroy)
|
37
|
+
subscription = App['live_record_' + this.modelName() + '_' + this.id()] = App.cable.subscriptions.create({
|
38
|
+
channel: 'LiveRecordChannel'
|
39
|
+
model_name: this.modelName()
|
40
|
+
record_id: this.id()
|
41
|
+
},
|
42
|
+
record: ->
|
43
|
+
return @_record if @_record
|
44
|
+
identifier = JSON.parse(this.identifier)
|
45
|
+
@_record = Model.all[identifier.record_id]
|
46
|
+
|
47
|
+
# on: connect
|
48
|
+
connected: ->
|
49
|
+
if @record()._staleSince != undefined
|
50
|
+
@syncRecord(@record())
|
51
|
+
|
52
|
+
@record()._callCallbacks('on:connect', undefined)
|
53
|
+
|
54
|
+
# on: disconnect
|
55
|
+
disconnected: ->
|
56
|
+
@record()._staleSince = (new Date()).toISOString() unless @record()._staleSince
|
57
|
+
@record()._callCallbacks('on:disconnect', undefined)
|
58
|
+
|
59
|
+
# on: receive
|
60
|
+
received: (data) ->
|
61
|
+
if data.error
|
62
|
+
@record()._staleSince = (new Date()).toISOString() unless @record()._staleSince
|
63
|
+
@onError[data.error.code].call(this, data)
|
64
|
+
@record()._callCallbacks('on:response_error', [data.error.code])
|
65
|
+
delete @record()['subscription']
|
66
|
+
else
|
67
|
+
@onAction[data.action].call(this, data)
|
68
|
+
|
69
|
+
# handler for received() callback above
|
70
|
+
onAction:
|
71
|
+
update: (data) ->
|
72
|
+
@record().update(data.attributes)
|
73
|
+
|
74
|
+
destroy: (data) ->
|
75
|
+
@record().destroy()
|
76
|
+
|
77
|
+
# handler for received() callback above
|
78
|
+
onError:
|
79
|
+
forbidden: (data) ->
|
80
|
+
console.error('[LiveRecord Response Error]', data.error.code, ':', data.error.message, 'for', @record())
|
81
|
+
bad_request: (data) ->
|
82
|
+
console.error('[LiveRecord Response Error]', data.error.code, ':', data.error.message, 'for', @record())
|
83
|
+
|
84
|
+
# syncs local record from remote record
|
85
|
+
syncRecord: ->
|
86
|
+
@perform(
|
87
|
+
'sync_record',
|
88
|
+
model_name: @record().modelName(),
|
89
|
+
record_id: @record().id(),
|
90
|
+
stale_since: @record()._staleSince
|
91
|
+
)
|
92
|
+
@record()._staleSince = undefined
|
93
|
+
)
|
94
|
+
|
95
|
+
this.subscription = subscription
|
96
|
+
|
97
|
+
Model.prototype.unsubscribe = ->
|
98
|
+
return if this.subscription == undefined
|
99
|
+
App.cable.subscriptions.remove(this.subscription)
|
100
|
+
delete this['subscription']
|
101
|
+
|
102
|
+
Model.prototype.isSubscribed = ->
|
103
|
+
this.subscription != undefined
|
104
|
+
|
105
|
+
# ALL
|
106
|
+
Model.all = {}
|
107
|
+
|
108
|
+
# CREATE
|
109
|
+
Model.prototype.create = () ->
|
110
|
+
this._callCallbacks('before:create', undefined)
|
111
|
+
|
112
|
+
Model.all[this.attributes.id] = this
|
113
|
+
this.subscribe()
|
114
|
+
|
115
|
+
this._callCallbacks('after:create', undefined)
|
116
|
+
this
|
117
|
+
|
118
|
+
# UPDATE
|
119
|
+
Model.prototype.update = (attributes) ->
|
120
|
+
this._callCallbacks('before:update', undefined)
|
121
|
+
|
122
|
+
self = this
|
123
|
+
Object.keys(attributes).forEach (attribute_key) ->
|
124
|
+
self.attributes[attribute_key] = attributes[attribute_key]
|
125
|
+
|
126
|
+
this._callCallbacks('after:update', undefined)
|
127
|
+
true
|
128
|
+
|
129
|
+
# DESTROY
|
130
|
+
Model.prototype.destroy = ->
|
131
|
+
this._callCallbacks('before:destroy', undefined)
|
132
|
+
|
133
|
+
this.unsubscribe()
|
134
|
+
delete Model.all[this.attributes.id]
|
135
|
+
|
136
|
+
this._callCallbacks('after:destroy', undefined)
|
137
|
+
this
|
138
|
+
|
139
|
+
# CALLBACKS
|
140
|
+
|
141
|
+
## class callbacks
|
142
|
+
Model._callbacks = {
|
143
|
+
'on:connect': [],
|
144
|
+
'on:disconnect': [],
|
145
|
+
'on:response_error': [],
|
146
|
+
'before:create': [],
|
147
|
+
'after:create': [],
|
148
|
+
'before:update': [],
|
149
|
+
'after:update': [],
|
150
|
+
'before:destroy': [],
|
151
|
+
'after:destroy': []
|
152
|
+
}
|
153
|
+
|
154
|
+
Model.addCallback = Model.prototype.addCallback = (callbackKey, callbackFunction) ->
|
155
|
+
index = this._callbacks[callbackKey].indexOf(callbackFunction)
|
156
|
+
if index == -1
|
157
|
+
this._callbacks[callbackKey].push(callbackFunction)
|
158
|
+
return callbackFunction
|
159
|
+
|
160
|
+
Model.removeCallback = Model.prototype.removeCallback = (callbackKey, callbackFunction) ->
|
161
|
+
index = this._callbacks[callbackKey].indexOf(callbackFunction)
|
162
|
+
if index != -1
|
163
|
+
this._callbacks[callbackKey].splice(index, 1)
|
164
|
+
return callbackFunction
|
165
|
+
|
166
|
+
Model.prototype._callCallbacks = (callbackKey, args) ->
|
167
|
+
# call class callbacks
|
168
|
+
for callback in Model._callbacks[callbackKey]
|
169
|
+
callback.apply(this, args)
|
170
|
+
|
171
|
+
# call instance callbacks
|
172
|
+
for callback in this._callbacks[callbackKey]
|
173
|
+
callback.apply(this, args)
|
174
|
+
|
175
|
+
# AFTER MODEL INITIALISATION
|
176
|
+
|
177
|
+
# add callbacks from arguments
|
178
|
+
for callbackKey, callbackFunctions of config.callbacks
|
179
|
+
for callbackFunction in callbackFunctions
|
180
|
+
Model.addCallback(callbackKey, callbackFunction)
|
181
|
+
|
182
|
+
|
183
|
+
# enable plugins from arguments
|
184
|
+
for pluginKey, pluginValue of config.plugins
|
185
|
+
if LiveRecord.plugins
|
186
|
+
index = Object.keys(LiveRecord.plugins).indexOf(pluginKey)
|
187
|
+
LiveRecord.plugins[pluginKey].applyToModel(Model, pluginValue) if index != -1
|
188
|
+
|
189
|
+
# add new Model to collection
|
190
|
+
LiveRecord.Model.all[config.modelName] = Model
|
191
|
+
|
192
|
+
Model
|
@@ -0,0 +1,18 @@
|
|
1
|
+
this.LiveRecord.plugins.LiveDOM || (this.LiveRecord.plugins.LiveDOM = {});
|
2
|
+
|
3
|
+
LiveRecord.plugins.LiveDOM.applyToModel = (Model, pluginValue) ->
|
4
|
+
return if pluginValue != true
|
5
|
+
|
6
|
+
# DOM callbacks
|
7
|
+
|
8
|
+
Model._updateDomCallback = ->
|
9
|
+
$updateableElements = $('[data-live-record-update-from]')
|
10
|
+
|
11
|
+
for key, value of this.attributes
|
12
|
+
$updateableElements.filter('[data-live-record-update-from="' + Model.modelName + '-' + this.id() + '-' + key + '"]').text(this[key]())
|
13
|
+
|
14
|
+
Model._destroyDomCallback = ->
|
15
|
+
$('[data-live-record-destroy-from="' + Model.modelName + '-' + this.id() + '"]').remove()
|
16
|
+
|
17
|
+
Model.addCallback('after:update', Model._updateDomCallback)
|
18
|
+
Model.addCallback('after:destroy', Model._destroyDomCallback)
|
data/lib/live_record.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module LiveRecord
|
2
|
+
module Channel
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
def subscribed
|
7
|
+
find_record_from_params(params) do |record|
|
8
|
+
authorised_attributes = authorised_attributes(record, current_user)
|
9
|
+
|
10
|
+
if authorised_attributes.present?
|
11
|
+
stream_for record, coder: ActiveSupport::JSON do |message|
|
12
|
+
record.reload
|
13
|
+
authorised_attributes = authorised_attributes(record, current_user)
|
14
|
+
|
15
|
+
if authorised_attributes.present?
|
16
|
+
response = filtered_message(message, authorised_attributes)
|
17
|
+
transmit response if response.present?
|
18
|
+
else
|
19
|
+
respond_with_error(:forbidden)
|
20
|
+
reject_subscription
|
21
|
+
end
|
22
|
+
end
|
23
|
+
else
|
24
|
+
respond_with_error(:forbidden)
|
25
|
+
reject
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def sync_record(data)
|
31
|
+
find_record_from_params(data.symbolize_keys) do |record|
|
32
|
+
authorised_attributes = authorised_attributes(record, current_user)
|
33
|
+
|
34
|
+
if authorised_attributes.present?
|
35
|
+
live_record_update = LiveRecordUpdate.where(
|
36
|
+
recordable_type: record.class.name,
|
37
|
+
recordable_id: record.id
|
38
|
+
).where(
|
39
|
+
'created_at >= ?', DateTime.parse(data['stale_since']) - 1.minute
|
40
|
+
).order(id: :asc)
|
41
|
+
|
42
|
+
if live_record_update.exists?
|
43
|
+
message = { 'action' => 'update', 'attributes' => record.attributes }
|
44
|
+
response = filtered_message(message, authorised_attributes)
|
45
|
+
transmit response if response.present?
|
46
|
+
end
|
47
|
+
else
|
48
|
+
respond_with_error(:forbidden)
|
49
|
+
reject_subscription
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def unsubscribed
|
55
|
+
# Any cleanup needed when channel is unsubscribed
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def authorised_attributes(record, current_user)
|
61
|
+
return record.class.live_record_whitelisted_attributes(record, current_user).map(&:to_s)
|
62
|
+
end
|
63
|
+
|
64
|
+
def filtered_message(message, filters)
|
65
|
+
if message['attributes'].present?
|
66
|
+
message['attributes'].slice!(*filters)
|
67
|
+
end
|
68
|
+
message
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_record_from_params(params)
|
72
|
+
model_class = params[:model_name].safe_constantize
|
73
|
+
|
74
|
+
if model_class && model_class < ApplicationRecord
|
75
|
+
record = model_class.find_by(id: params[:record_id])
|
76
|
+
|
77
|
+
if record.present?
|
78
|
+
yield record
|
79
|
+
else
|
80
|
+
transmit 'action' => 'destroy'
|
81
|
+
end
|
82
|
+
else
|
83
|
+
respond_with_error(:bad_request, 'Not a correct model name')
|
84
|
+
reject_subscription
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def respond_with_error(type, message = nil)
|
89
|
+
case type
|
90
|
+
when :forbidden
|
91
|
+
transmit error: { 'code' => 'forbidden', 'message' => (message || 'You are not authorised') }
|
92
|
+
when :bad_request
|
93
|
+
transmit error: { 'code' => 'bad_request', 'message' => (message || 'Invalid request parameters') }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module LiveRecord
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
desc 'Copy LiveRecord Javascript template'
|
7
|
+
source_root File.expand_path('../templates', __FILE__)
|
8
|
+
|
9
|
+
class_option :live_dom, desc: 'Enables LiveDom plugin: [true, false]', default: 'true'
|
10
|
+
class_option :javascript_engine, desc: 'JS engine to be used: [js, coffee]'
|
11
|
+
class_option :template_engine, desc: 'Template engine to be used (if LiveDom plugin enabled): [erb, slim, haml]'
|
12
|
+
|
13
|
+
def copy_assets_javascript_template
|
14
|
+
copy_file "javascript.#{javascript_engine}.rb", "lib/templates/#{javascript_engine}/assets/javascript.#{javascript_engine}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def copy_model_template
|
18
|
+
copy_file "model.rb.rb", "lib/templates/active_record/model/model.rb"
|
19
|
+
end
|
20
|
+
|
21
|
+
def copy_scaffold_index_template
|
22
|
+
copy_file "index.html.#{template_engine}", "lib/templates/#{template_engine}/scaffold/index.html.#{template_engine}" if live_dom
|
23
|
+
end
|
24
|
+
|
25
|
+
def copy_scaffold_show_template
|
26
|
+
copy_file "show.html.#{template_engine}", "lib/templates/#{template_engine}/scaffold/show.html.#{template_engine}" if live_dom
|
27
|
+
end
|
28
|
+
|
29
|
+
def copy_live_record_update_model_template
|
30
|
+
class_collisions 'LiveRecordUpdate'
|
31
|
+
template 'live_record_update.rb', File.join('app/models', 'live_record_update.rb')
|
32
|
+
migration_template 'create_live_record_updates.rb', 'db/migrate/create_live_record_updates.rb'
|
33
|
+
end
|
34
|
+
|
35
|
+
def copy_live_record_channel_template
|
36
|
+
class_collisions 'LiveRecordChannel'
|
37
|
+
template 'live_record_channel.rb', File.join('app/channels', 'live_record_channel.rb')
|
38
|
+
end
|
39
|
+
|
40
|
+
def update_application_record
|
41
|
+
in_root do
|
42
|
+
insert_into_file 'app/models/application_record.rb', " include LiveRecord::Model\n", after: "self.abstract_class = true\n"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def update_application_javascript
|
47
|
+
in_root do
|
48
|
+
insert_into_file 'app/assets/javascripts/application.js', "//= require live_record\n", before: "//= require_tree ."
|
49
|
+
insert_into_file 'app/assets/javascripts/application.js', "//= require live_record/plugins/live_dom\n", before: "//= require_tree ." if live_dom
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def self.next_migration_number(dir)
|
56
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
57
|
+
end
|
58
|
+
|
59
|
+
def javascript_engine
|
60
|
+
Rails.application.config.generators.options[:rails][:javascript_engine] || options[:javascript_engine]
|
61
|
+
end
|
62
|
+
|
63
|
+
def template_engine
|
64
|
+
Rails.application.config.generators.options[:rails][:template_engine] || options[:template_engine]
|
65
|
+
end
|
66
|
+
|
67
|
+
def live_dom
|
68
|
+
options[:live_dom] == 'true' ? true : options[:live_dom] == 'false' ? false : raise(ArgumentError, 'invalid value for --live_dom. Can only be `true` or `false`')
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<script>
|
2
|
+
LiveRecord.helpers.loadRecords({modelName: '<%= singular_table_name.camelcase %>'})
|
3
|
+
</script>
|
4
|
+
<p id="notice"><%%= notice %></p>
|
5
|
+
|
6
|
+
<h1><%= plural_table_name.titleize %></h1>
|
7
|
+
|
8
|
+
<table>
|
9
|
+
<thead>
|
10
|
+
<tr>
|
11
|
+
<% attributes.reject(&:password_digest?).each do |attribute| -%>
|
12
|
+
<th><%= attribute.human_name %></th>
|
13
|
+
<% end -%>
|
14
|
+
<th colspan="3"></th>
|
15
|
+
</tr>
|
16
|
+
</thead>
|
17
|
+
|
18
|
+
<tbody>
|
19
|
+
<%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %>
|
20
|
+
<tr data-live-record-destroy-from='<%= singular_table_name.camelcase %>-<%%= <%= singular_table_name %>.id %>'>
|
21
|
+
<% attributes.reject(&:password_digest?).each do |attribute| -%>
|
22
|
+
<td data-live-record-update-from='<%= singular_table_name.camelcase %>-<%%= <%= singular_table_name %>.id %>-<%= attribute.name %>'><%%= <%= singular_table_name %>.<%= attribute.name %> %></td>
|
23
|
+
<% end -%>
|
24
|
+
<td><%%= link_to 'Show', <%= singular_table_name %> %></td>
|
25
|
+
<td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td>
|
26
|
+
<td><%%= link_to 'Destroy', <%= singular_table_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td>
|
27
|
+
</tr>
|
28
|
+
<%% end %>
|
29
|
+
</tbody>
|
30
|
+
</table>
|
31
|
+
|
32
|
+
<br>
|
33
|
+
|
34
|
+
<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_table_name %>_path %>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<% module_namespacing do -%>
|
2
|
+
LiveRecord.Model.create(
|
3
|
+
{
|
4
|
+
modelName: '<%= singular_table_name.camelcase %>',
|
5
|
+
plugins: {
|
6
|
+
LiveDOM: true
|
7
|
+
},
|
8
|
+
# See TODO: URL_TO_DOCUMENTATION for supported callbacks
|
9
|
+
# Add Callbacks (callback name => array of functions)
|
10
|
+
# callbacks: {
|
11
|
+
# 'on:disconnect': [],
|
12
|
+
# 'after:update': [],
|
13
|
+
# }
|
14
|
+
}
|
15
|
+
)
|
16
|
+
<% end -%>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<% module_namespacing do -%>
|
2
|
+
LiveRecord.Model.create(
|
3
|
+
{
|
4
|
+
modelName: '<%= file_name.singularize.camelcase %>',
|
5
|
+
plugins: {
|
6
|
+
LiveDOM: true
|
7
|
+
},
|
8
|
+
// See TODO: URL_TO_DOCUMENTATION for supported callbacks
|
9
|
+
// Add Callbacks (callback name => array of functions)
|
10
|
+
// callbacks: {
|
11
|
+
// 'on:disconnect': [],
|
12
|
+
// 'after:update': [],
|
13
|
+
// }
|
14
|
+
}
|
15
|
+
)
|
16
|
+
<% end -%>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<% module_namespacing do -%>
|
2
|
+
class <%= class_name %> < <%= parent_class_name.classify %>
|
3
|
+
<% attributes.select(&:reference?).each do |attribute| -%>
|
4
|
+
belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %>
|
5
|
+
<% end -%>
|
6
|
+
<% attributes.select(&:token?).each do |attribute| -%>
|
7
|
+
has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
|
8
|
+
<% end -%>
|
9
|
+
<% if attributes.any?(&:password_digest?) -%>
|
10
|
+
has_secure_password
|
11
|
+
<% end -%>
|
12
|
+
|
13
|
+
def self.live_record_whitelisted_attributes(<%= class_name.underscore %>, current_user)
|
14
|
+
# Add attributes to this array that you would like current_user to have access to.
|
15
|
+
# Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
|
16
|
+
[]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
<% end -%>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<script>
|
2
|
+
LiveRecord.helpers.loadRecords({modelName: '<%= singular_table_name.camelcase %>'})
|
3
|
+
</script>
|
4
|
+
<p id="notice"><%%= notice %></p>
|
5
|
+
|
6
|
+
<% attributes.reject(&:password_digest?).each do |attribute| -%>
|
7
|
+
<p>
|
8
|
+
<strong><%= attribute.human_name %>:</strong>
|
9
|
+
<span data-live-record-update-from='<%= singular_table_name.camelcase %>-<%%= @<%= singular_table_name %>.id %>-<%= attribute.name %>'>
|
10
|
+
<%%= @<%= singular_table_name %>.<%= attribute.name %> %>
|
11
|
+
</span>
|
12
|
+
</p>
|
13
|
+
|
14
|
+
<% end -%>
|
15
|
+
<%%= link_to 'Edit', edit_<%= singular_table_name %>_path(@<%= singular_table_name %>) %> |
|
16
|
+
<%%= link_to 'Back', <%= index_helper %>_path %>
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module LiveRecord
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
has_many :live_record_updates, as: :recordable
|
7
|
+
|
8
|
+
after_update :__live_record_reference_changed_attributes__
|
9
|
+
after_update_commit :__live_record_broadcast_record_update__
|
10
|
+
after_destroy_commit :__live_record_broadcast_record_destroy__
|
11
|
+
|
12
|
+
def self.live_record_whitelisted_attributes(record, current_user)
|
13
|
+
[]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def __live_record_reference_changed_attributes__
|
19
|
+
@_live_record_changed_attributes = changed
|
20
|
+
end
|
21
|
+
|
22
|
+
def __live_record_broadcast_record_update__
|
23
|
+
included_attributes = attributes.slice(*@_live_record_changed_attributes)
|
24
|
+
@_live_record_changed_attributes = nil
|
25
|
+
message_data = { 'action' => 'update', 'attributes' => included_attributes }
|
26
|
+
LiveRecordChannel.broadcast_to(self, message_data)
|
27
|
+
LiveRecordUpdate.create!(recordable: self)
|
28
|
+
end
|
29
|
+
|
30
|
+
def __live_record_broadcast_record_destroy__
|
31
|
+
message_data = { 'action' => 'destroy' }
|
32
|
+
LiveRecordChannel.broadcast_to(self, message_data)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/live_record.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'live_record/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'live_record'
|
7
|
+
s.version = LiveRecord::VERSION
|
8
|
+
s.date = '2017-08-03'
|
9
|
+
s.summary = 'Rails 5 ActionCable Live JS Objects and DOM Elements'
|
10
|
+
s.description = "Auto-syncs records in client-side JS (through a Model DSL) from changes in the backend Rails server through ActionCable.\nAuto-updates DOM elements mapped to a record attribute, from changes.\nAutomatically resyncs after client-side reconnection."
|
11
|
+
s.authors = ['Jules Roman B. Polidario']
|
12
|
+
s.email = 'jrpolidario@gmail.com'
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.homepage = 'https://github.com/jrpolidario/live_record'
|
15
|
+
s.license = 'MIT'
|
16
|
+
s.required_ruby_version = '~> 2.0'
|
17
|
+
|
18
|
+
s.add_dependency 'rails', '~> 5.0', '< 5.2'
|
19
|
+
s.add_development_dependency 'byebug', '~> 9.0'
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: live_record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jules Roman B. Polidario
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-08-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.0'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '5.2'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '5.0'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '5.2'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: byebug
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '9.0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '9.0'
|
47
|
+
description: |-
|
48
|
+
Auto-syncs records in client-side JS (through a Model DSL) from changes in the backend Rails server through ActionCable.
|
49
|
+
Auto-updates DOM elements mapped to a record attribute, from changes.
|
50
|
+
Automatically resyncs after client-side reconnection.
|
51
|
+
email: jrpolidario@gmail.com
|
52
|
+
executables: []
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files: []
|
55
|
+
files:
|
56
|
+
- ".gitignore"
|
57
|
+
- Gemfile
|
58
|
+
- Gemfile.lock
|
59
|
+
- README.md
|
60
|
+
- app/assets/javascripts/live_record.js
|
61
|
+
- app/assets/javascripts/live_record/helpers.js
|
62
|
+
- app/assets/javascripts/live_record/helpers/load_records.coffee
|
63
|
+
- app/assets/javascripts/live_record/model.js
|
64
|
+
- app/assets/javascripts/live_record/model/all.coffee
|
65
|
+
- app/assets/javascripts/live_record/model/create.coffee
|
66
|
+
- app/assets/javascripts/live_record/plugins.js
|
67
|
+
- app/assets/javascripts/live_record/plugins/live_dom.coffee
|
68
|
+
- lib/live_record.rb
|
69
|
+
- lib/live_record/channel.rb
|
70
|
+
- lib/live_record/engine.rb
|
71
|
+
- lib/live_record/generators/install_generator.rb
|
72
|
+
- lib/live_record/generators/templates/create_live_record_updates.rb
|
73
|
+
- lib/live_record/generators/templates/index.html.erb
|
74
|
+
- lib/live_record/generators/templates/javascript.coffee.rb
|
75
|
+
- lib/live_record/generators/templates/javascript.js.rb
|
76
|
+
- lib/live_record/generators/templates/live_record_channel.rb
|
77
|
+
- lib/live_record/generators/templates/live_record_update.rb
|
78
|
+
- lib/live_record/generators/templates/model.rb.rb
|
79
|
+
- lib/live_record/generators/templates/show.html.erb
|
80
|
+
- lib/live_record/model.rb
|
81
|
+
- lib/live_record/version.rb
|
82
|
+
- live_record.gemspec
|
83
|
+
homepage: https://github.com/jrpolidario/live_record
|
84
|
+
licenses:
|
85
|
+
- MIT
|
86
|
+
metadata: {}
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '2.0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 2.6.11
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: Rails 5 ActionCable Live JS Objects and DOM Elements
|
107
|
+
test_files: []
|