live_record 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|