live_record 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -2
- data/{lib/live_record/.rspec → .rspec} +0 -0
- data/.travis.yml +0 -1
- data/Gemfile +3 -1
- data/README.md +363 -202
- data/Rakefile +5 -0
- data/app/assets/javascripts/live_record.coffee +4 -0
- data/app/assets/javascripts/live_record/helpers.coffee +4 -0
- data/app/assets/javascripts/live_record/helpers/load_records.coffee +13 -7
- data/app/assets/javascripts/live_record/helpers/spaceship.coffee +13 -0
- data/app/assets/javascripts/live_record/model.coffee +4 -0
- data/app/assets/javascripts/live_record/model/create.coffee +96 -27
- data/app/assets/javascripts/live_record/plugins.coffee +3 -0
- data/app/assets/javascripts/live_record/plugins/live_dom.coffee +5 -19
- data/app/assets/javascripts/live_record/plugins/live_dom/apply_to_model.coffee +17 -0
- data/app/channels/live_record/base_channel.rb +41 -0
- data/app/channels/live_record/changes_channel.rb +59 -0
- data/app/channels/live_record/publications_channel.rb +134 -0
- data/{lib/live_record/config.ru → config.ru} +0 -0
- data/lib/live_record.rb +2 -0
- data/lib/live_record/action_view_extensions/view_helper.rb +22 -0
- data/lib/live_record/configure.rb +19 -0
- data/lib/live_record/generators/install_generator.rb +9 -4
- data/lib/live_record/generators/templates/create_live_record_updates.rb +1 -1
- data/lib/live_record/generators/templates/index.html.erb +1 -0
- data/lib/live_record/generators/templates/model.rb.rb +4 -4
- data/lib/live_record/model/callbacks.rb +8 -2
- data/lib/live_record/version.rb +1 -1
- data/live_record.gemspec +2 -2
- data/{lib/live_record/spec → spec}/factories/posts.rb +0 -0
- data/spec/features/live_record_syncing_spec.rb +184 -0
- data/spec/helpers/wait.rb +19 -0
- data/{lib/live_record/spec → spec}/internal/app/assets/config/manifest.js +0 -0
- data/{lib/live_record/spec → spec}/internal/app/assets/javascripts/application.js +0 -0
- data/{lib/live_record/spec → spec}/internal/app/assets/javascripts/cable.js +0 -0
- data/spec/internal/app/assets/javascripts/posts.coffee +8 -0
- data/{lib/live_record/spec → spec}/internal/app/channels/application_cable/channel.rb +0 -0
- data/{lib/live_record/spec → spec}/internal/app/channels/application_cable/connection.rb +4 -4
- data/{lib/live_record/spec → spec}/internal/app/controllers/application_controller.rb +0 -0
- data/{lib/live_record/spec → spec}/internal/app/controllers/posts_controller.rb +1 -0
- data/{lib/live_record/spec → spec}/internal/app/models/application_record.rb +0 -0
- data/{lib/live_record/spec → spec}/internal/app/models/live_record_update.rb +0 -0
- data/spec/internal/app/models/post.rb +11 -0
- data/{lib/live_record/spec → spec}/internal/app/views/layouts/application.html.erb +0 -0
- data/{lib/live_record/spec → spec}/internal/app/views/posts/_form.html.erb +0 -0
- data/{lib/live_record/spec → spec}/internal/app/views/posts/_post.json.jbuilder +0 -0
- data/{lib/live_record/spec → spec}/internal/app/views/posts/edit.html.erb +0 -0
- data/{lib/live_record/spec → spec}/internal/app/views/posts/index.html.erb +6 -5
- data/{lib/live_record/spec → spec}/internal/app/views/posts/index.json.jbuilder +0 -0
- data/{lib/live_record/spec → spec}/internal/app/views/posts/new.html.erb +0 -0
- data/{lib/live_record/spec → spec}/internal/app/views/posts/show.html.erb +0 -0
- data/{lib/live_record/spec → spec}/internal/app/views/posts/show.json.jbuilder +0 -0
- data/{lib/live_record/spec → spec}/internal/config/cable.yml +0 -0
- data/{lib/live_record/spec → spec}/internal/config/database.yml +0 -0
- data/{lib/live_record/spec → spec}/internal/config/routes.rb +0 -0
- data/{lib/live_record/spec → spec}/internal/db/schema.rb +2 -0
- data/{lib/live_record/spec → spec}/internal/public/favicon.ico +0 -0
- data/{lib/live_record/spec → spec}/rails_helper.rb +4 -2
- data/{lib/live_record/spec → spec}/spec_helper.rb +0 -0
- metadata +64 -56
- data/app/assets/javascripts/live_record.js +0 -4
- data/app/assets/javascripts/live_record/helpers.js +0 -4
- data/app/assets/javascripts/live_record/model.js +0 -4
- data/app/assets/javascripts/live_record/plugins.js +0 -3
- data/lib/live_record/channel/implement.rb +0 -100
- data/lib/live_record/spec/features/live_record_syncing_spec.rb +0 -60
- data/lib/live_record/spec/internal/app/assets/javascripts/posts.coffee +0 -14
- data/lib/live_record/spec/internal/app/channels/live_record_channel.rb +0 -4
- data/lib/live_record/spec/internal/app/models/post.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68e46da08f168efb9e1ebfb51f075548aaaa42cd
|
4
|
+
data.tar.gz: fbb81c2b262d59fa5b05e7a8b1a84bb20962538f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c3a0ffcac47a14e301671222403391380ece8577cf2a62ec1477b604155a31fb660b5fcc2e3106ebf2e94245c77e38b32aaf83d7c5e0d24ca1617fdd19370e3
|
7
|
+
data.tar.gz: 95949617075641208bbf35ace36222c43f4d11bb415110ea1da45ef05281191b28dc80e4136dadf9ff71f6f4f4836ae14b102403ae2e53973e49fbdae5992338
|
data/.gitignore
CHANGED
File without changes
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -5,5 +5,7 @@ gemspec
|
|
5
5
|
group :development, :test do
|
6
6
|
# issues with Combustion + FactoryGirl factory loading: https://github.com/pat/combustion/issues/33
|
7
7
|
# therefore, this gem should not be part of .gemspec but instead is specified here in the Gemfile
|
8
|
-
gem 'factory_girl_rails', require: false
|
8
|
+
gem 'factory_girl_rails', '~> 4.8', require: false
|
9
|
+
# do not require to prevent Capybara deprecation warning on rspec run
|
10
|
+
gem 'capybara', '~> 2.15', require: false
|
9
11
|
end
|
data/README.md
CHANGED
@@ -2,12 +2,16 @@
|
|
2
2
|
|
3
3
|
## About
|
4
4
|
|
5
|
-
* Auto-syncs records in client-side JS (through a Model DSL) from changes in the backend Rails server through ActionCable
|
6
|
-
*
|
7
|
-
*
|
5
|
+
* Auto-syncs records in client-side JS (through a Model DSL) from changes (updates/destroy) in the backend Rails server through ActionCable.
|
6
|
+
* Also supports streaming newly created records to client-side JS
|
7
|
+
* Supports lost connection restreaming for both new records (create), and record-changes (updates/destroy).
|
8
|
+
* Auto-updates DOM elements mapped to a record attribute, from changes (updates/destroy). **(Optional LiveDOM Plugin)**
|
8
9
|
|
9
10
|
> `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
|
|
12
|
+
*New Version 0.2!*
|
13
|
+
*See [Changelog below](#changelog)*
|
14
|
+
|
11
15
|
## Requirements
|
12
16
|
|
13
17
|
* **>= Ruby 2.2.2**
|
@@ -19,18 +23,38 @@
|
|
19
23
|
|
20
24
|
## Usage Example
|
21
25
|
|
22
|
-
*
|
26
|
+
* say we have a `Book` model which has the following attributes:
|
27
|
+
* `title:string`
|
28
|
+
* `author:text`
|
29
|
+
* `is_enabled:boolean`
|
30
|
+
* on the JS client-side:
|
23
31
|
|
32
|
+
### Subscribing to Record Creation
|
24
33
|
```js
|
25
|
-
//
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
+
// subscribe, and auto-receive newly created Book records from the Rails server
|
35
|
+
LiveRecord.Model.all.Book.subscribe()
|
36
|
+
|
37
|
+
// ...or only those which are enabled
|
38
|
+
// LiveRecord.Model.all.Book.subscribe({where: {is_enabled_eq: true}})
|
39
|
+
|
40
|
+
// now, we can just simply add a "create" callback, to apply our own logic whenever a new Book record is streamed from the backend
|
41
|
+
LiveRecord.Model.all.Book.addCallback('after:create', function() {
|
42
|
+
// let's say you have a code here that adds this new Book on the page
|
43
|
+
// `this` refers to the Book record that has been created
|
44
|
+
console.log(this);
|
45
|
+
})
|
46
|
+
```
|
47
|
+
|
48
|
+
### Subscribing to Record Updates/Destroy
|
49
|
+
|
50
|
+
```js
|
51
|
+
// instantiate a Book object (only requirement is you pass the ID so it can be referenced when updates/destroy happen)
|
52
|
+
var book = new LiveRecord.Model.all.Book({id: 1})
|
53
|
+
|
54
|
+
// ...or you can also initialise with other attributes
|
55
|
+
// var book = new LiveRecord.Model.all.Book({id: 1, title: 'Harry Potter', created_at: '2017-08-02T12:39:49.238Z'})
|
56
|
+
|
57
|
+
// then store this Book object into the JS store
|
34
58
|
book.create();
|
35
59
|
|
36
60
|
// the store is accessible through
|
@@ -38,7 +62,15 @@
|
|
38
62
|
|
39
63
|
// all records in the JS store are automatically subscribed to the backend LiveRecordChannel, which meant syncing (update / destroy) changes from the backend
|
40
64
|
|
41
|
-
//
|
65
|
+
// because the `book` above is already created in the store, you'll notice that it should automatically sync itself including all other possible whitelisted attributes
|
66
|
+
console.log(book.attributes);
|
67
|
+
// at this point then, console.log above will show the following on your browser console:
|
68
|
+
// {id: 1, title: 'Harry Potter', author: 'J.K. Rowling', is_enabled: true, created_at: '2017-08-02T12:39:49.238Z', updated_at: '2017-08-02T12:39:49.238Z'}
|
69
|
+
|
70
|
+
// All attributes automatically updates itself so you'll be sure that the following line (for example) is always up-to-date
|
71
|
+
console.log(book.updated_at())
|
72
|
+
|
73
|
+
// you can also add a callback that will be invoked whenever the Book object has been updated (see all supported callbacks further below)
|
42
74
|
book.addCallback('after:update', function() {
|
43
75
|
// let's say you update the DOM elements here when the attributes have changed
|
44
76
|
// `this` refers to the Book record that has been updated
|
@@ -74,224 +106,305 @@
|
|
74
106
|
end
|
75
107
|
```
|
76
108
|
|
77
|
-
* 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
|
109
|
+
* whenever a Book (or any other Model record that you specified) has been created / updated / destroyed, there exists an `after_create_commit`, `after_update_commit` and an `after_destroy_commit` ActiveRecord callback that will broadcast changes to all subscribed JS clients
|
78
110
|
|
79
111
|
## Setup
|
80
|
-
|
112
|
+
1. Add the following to your `Gemfile`:
|
81
113
|
|
82
|
-
|
83
|
-
|
84
|
-
|
114
|
+
```ruby
|
115
|
+
gem 'live_record', '~> 0.2.0'
|
116
|
+
```
|
85
117
|
|
86
|
-
|
118
|
+
2. Run:
|
87
119
|
|
88
|
-
|
89
|
-
|
90
|
-
|
120
|
+
```bash
|
121
|
+
bundle install
|
122
|
+
```
|
91
123
|
|
92
|
-
|
124
|
+
3. Install by running:
|
93
125
|
|
94
|
-
|
95
|
-
|
96
|
-
|
126
|
+
```bash
|
127
|
+
rails generate live_record:install
|
128
|
+
```
|
97
129
|
|
98
|
-
|
130
|
+
> `rails generate live_record:install --live_dom=false` if you do not need the `LiveDOM` plugin; `--live_dom=true` by default
|
99
131
|
|
100
|
-
|
132
|
+
4. Run migration to create the `live_record_updates` table, which is going to be used for client reconnection resyncing:
|
101
133
|
|
102
134
|
```bash
|
103
135
|
rake db:migrate
|
104
136
|
```
|
105
137
|
|
106
|
-
|
138
|
+
5. Update your **app/channels/application_cable/connection.rb**, and add `current_user` method, unless you already have it:
|
107
139
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
140
|
+
```ruby
|
141
|
+
module ApplicationCable
|
142
|
+
class Connection < ActionCable::Connection::Base
|
143
|
+
identified_by :current_user
|
112
144
|
|
113
|
-
|
114
|
-
|
115
|
-
|
145
|
+
def current_user
|
146
|
+
# write something here if you have a current_user, or you may just leave this blank. Example below when using `devise` gem:
|
147
|
+
# User.find_by(id: cookies.signed[:user_id])
|
148
|
+
end
|
116
149
|
end
|
117
150
|
end
|
118
|
-
|
119
|
-
```
|
151
|
+
```
|
120
152
|
|
121
|
-
|
153
|
+
6. Update your **model** files (only those you would want to be synced), and insert the following public method:
|
122
154
|
|
123
|
-
|
155
|
+
> automatically updated if you use Rails scaffold or model generator
|
124
156
|
|
125
|
-
|
157
|
+
### Example 1 - Simple Usage
|
126
158
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
# Add attributes to this array that you would like current_user to have access to when syncing.
|
132
|
-
# Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
|
133
|
-
[:title, :author, :created_at, :updated_at]
|
134
|
-
end
|
135
|
-
end
|
136
|
-
```
|
137
|
-
|
138
|
-
### Example 2 - Advanced Usage
|
159
|
+
```ruby
|
160
|
+
# app/models/book.rb (example 1)
|
161
|
+
class Book < ApplicationRecord
|
162
|
+
include LiveRecord::Model::Callbacks
|
139
163
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
def self.live_record_whitelisted_attributes(book, current_user)
|
144
|
-
# Notice that from above, you also have access to `book` (the record currently requested by the client to be synced),
|
145
|
-
# and the `current_user`, the current user who is trying to sync the `book` record.
|
146
|
-
if book.user == current_user
|
147
|
-
[:title, :author, :created_at, :updated_at, :reference_id, :origin_address]
|
148
|
-
elsif current_user.present?
|
164
|
+
def self.live_record_whitelisted_attributes(book, current_user)
|
165
|
+
# Add attributes to this array that you would like current_user to have access to when syncing.
|
166
|
+
# Defaults to empty array, thereby blocking everything by default, only unless explicitly stated here so.
|
149
167
|
[:title, :author, :created_at, :updated_at]
|
150
|
-
else
|
151
|
-
[]
|
152
168
|
end
|
153
169
|
end
|
154
|
-
|
155
|
-
|
170
|
+
```
|
171
|
+
|
172
|
+
### Example 2 - Advanced Usage
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
# app/models/book.rb (example 1)
|
176
|
+
class Book < ApplicationRecord
|
177
|
+
include LiveRecord::Model::Callbacks
|
178
|
+
|
179
|
+
def self.live_record_whitelisted_attributes(book, current_user)
|
180
|
+
# Notice that from above, you also have access to `book` (the record currently requested by the client to be synced),
|
181
|
+
# and the `current_user`, the current user who is trying to sync the `book` record.
|
182
|
+
if book.user == current_user
|
183
|
+
[:title, :author, :created_at, :updated_at, :reference_id, :origin_address]
|
184
|
+
elsif current_user.present?
|
185
|
+
[:title, :author, :created_at, :updated_at]
|
186
|
+
else
|
187
|
+
[]
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
```
|
156
192
|
|
157
|
-
|
193
|
+
7. For each Model you want to sync, insert the following in your Javascript files.
|
158
194
|
|
159
|
-
|
195
|
+
> automatically updated if you use Rails scaffold or controller generator
|
160
196
|
|
161
|
-
|
197
|
+
### Example 1 - Model
|
162
198
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
199
|
+
```js
|
200
|
+
// app/assets/javascripts/books.js
|
201
|
+
LiveRecord.Model.create(
|
202
|
+
{
|
203
|
+
modelName: 'Book' // should match the Rails model name
|
204
|
+
plugins: {
|
205
|
+
LiveDOM: true // remove this if you do not need `LiveDOM`
|
206
|
+
}
|
170
207
|
}
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
208
|
+
)
|
209
|
+
```
|
210
|
+
|
211
|
+
### Example 2 - Model + Callbacks
|
212
|
+
|
213
|
+
```js
|
214
|
+
// app/assets/javascripts/books.js
|
215
|
+
LiveRecord.Model.create(
|
216
|
+
{
|
217
|
+
modelName: 'Book',
|
218
|
+
callbacks: {
|
219
|
+
'on:connect': [
|
220
|
+
function() {
|
221
|
+
console.log(this); // `this` refers to the current `Book` record that has just connected for syncing
|
222
|
+
}
|
223
|
+
],
|
224
|
+
'after:update': [
|
225
|
+
function() {
|
226
|
+
console.log(this); // `this` refers to the current `Book` record that has just been updated with changes synced from the backend
|
227
|
+
}
|
228
|
+
]
|
229
|
+
}
|
193
230
|
}
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
231
|
+
)
|
232
|
+
```
|
233
|
+
|
234
|
+
#### Model Callbacks supported:
|
235
|
+
* `on:connect`
|
236
|
+
* `on:disconnect`
|
237
|
+
* `on:response_error`
|
238
|
+
* `before:create`
|
239
|
+
* `after:create`
|
240
|
+
* `before:update`
|
241
|
+
* `after:update`
|
242
|
+
* `before:destroy`
|
243
|
+
* `after:destroy`
|
244
|
+
|
245
|
+
> Each callback should map to an array of functions
|
246
|
+
|
247
|
+
* `on:response_error` supports a function argument: The "Error Code". i.e.
|
248
|
+
|
249
|
+
### Example 3 - Handling Response Error
|
250
|
+
|
251
|
+
```js
|
252
|
+
LiveRecord.Model.create(
|
253
|
+
{
|
254
|
+
modelName: 'Book',
|
255
|
+
callbacks: {
|
256
|
+
'on:response_error': [
|
257
|
+
function(errorCode) {
|
258
|
+
console.log(errorCode); // errorCode is a string, representing the type of error. See Response Error Codes below:
|
259
|
+
}
|
260
|
+
]
|
261
|
+
}
|
225
262
|
}
|
263
|
+
)
|
264
|
+
```
|
265
|
+
|
266
|
+
#### Response Error Codes:
|
267
|
+
* `"forbidden"` - Current User is not authorized to sync record changes. Happens when Model's `live_record_whitelisted_attributes` method returns empty array.
|
268
|
+
* `"bad_request"` - Happens when `LiveRecord.Model.create({modelName: 'INCORRECTMODELNAME'})`
|
269
|
+
|
270
|
+
8. Load the records into the JS Model-store through JSON REST (i.e.):
|
271
|
+
|
272
|
+
### Example 1 - Using Default Loader (Requires JQuery)
|
273
|
+
|
274
|
+
> Your controller must also support responding with JSON in addition to HTML. If you used scaffold or controller generator, this should already work immediately.
|
275
|
+
|
276
|
+
```html
|
277
|
+
<!-- app/views/books/index.html.erb -->
|
278
|
+
<script>
|
279
|
+
// `loadRecords` asynchronously loads all records (using the current URL) to the store, through a JSON AJAX request.
|
280
|
+
// in this example, `loadRecords` will load JSON from the current URL which is /books
|
281
|
+
LiveRecord.helpers.loadRecords({modelName: 'Book'})
|
282
|
+
</script>
|
283
|
+
```
|
284
|
+
|
285
|
+
```html
|
286
|
+
<!-- app/views/books/index.html.erb -->
|
287
|
+
<script>
|
288
|
+
// `loadRecords` you may also specify a URL to loadRecords (`url` defaults to `window.location.href` which is the current page)
|
289
|
+
LiveRecord.helpers.loadRecords({modelName: 'Book', url: '/some/url/that/returns_books_as_a_json'})
|
290
|
+
</script>
|
291
|
+
```
|
292
|
+
|
293
|
+
```html
|
294
|
+
<!-- app/views/posts/index.html.erb -->
|
295
|
+
<script>
|
296
|
+
// You may also pass in a callback for synchronous logic
|
297
|
+
LiveRecord.helpers.loadRecords({
|
298
|
+
modelName: 'Book',
|
299
|
+
onLoad: function(records) {
|
300
|
+
// ...
|
301
|
+
},
|
302
|
+
onError: function(jqxhr, textStatus, error) {
|
303
|
+
// ...
|
226
304
|
}
|
227
|
-
)
|
228
|
-
|
305
|
+
})
|
306
|
+
</script>
|
307
|
+
```
|
229
308
|
|
230
|
-
|
231
|
-
* `"forbidden"` - Current User is not authorized to sync record changes. Happens when Model's `live_record_whitelisted_attributes` method returns empty array.
|
232
|
-
* `"bad_request"` - Happens when `LiveRecord.Model.create({modelName: 'INCORRECTMODELNAME'})`
|
309
|
+
### Example 2 - Using Custom Loader
|
233
310
|
|
234
|
-
|
311
|
+
```js
|
312
|
+
// do something here that will fetch Book record attributes...
|
313
|
+
// as an example, say you already have the following attributes:
|
314
|
+
var book1Attributes = { id: 1, title: 'Noli Me Tangere', author: 'José Rizal' }
|
315
|
+
var book2Attributes = { id: 2, title: 'ABNKKBSNPLAko?!', author: 'Bob Ong' }
|
235
316
|
|
236
|
-
|
317
|
+
// then we instantiate a Book object
|
318
|
+
var book1 = new LiveRecord.Model.all.Book(book1Attributes);
|
319
|
+
// then we push this Book object to the Book store, which then automatically subscribes them to changes in the backend
|
320
|
+
book1.create();
|
237
321
|
|
238
|
-
|
322
|
+
var book2 = new LiveRecord.Model.all.Book(book2Attributes);
|
323
|
+
book2.create();
|
239
324
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
LiveRecord.helpers.loadRecords({modelName: 'Book'})
|
246
|
-
</script>
|
247
|
-
```
|
325
|
+
// you can also add Instance callbacks specific only to this Object (supported callbacks are the same as the Model callbacks)
|
326
|
+
book2.addCallback('after:update', function() {
|
327
|
+
// do something when book2 has been updated after syncing
|
328
|
+
})
|
329
|
+
```
|
248
330
|
|
249
|
-
|
250
|
-
<!-- app/views/books/index.html.erb -->
|
251
|
-
<script>
|
252
|
-
// `loadRecords` you may also specify a URL to loadRecords (`url` defaults to `window.location.href` which is the current page)
|
253
|
-
LiveRecord.helpers.loadRecords({modelName: 'Book', url: '/some/url/that/returns_books_as_a_json'})
|
254
|
-
</script>
|
255
|
-
```
|
331
|
+
9. To automatically receive new Book records, you may subscribe:
|
256
332
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
333
|
+
```js
|
334
|
+
// subscribe
|
335
|
+
subscription = LiveRecord.Model.all.Book.subscribe();
|
336
|
+
|
337
|
+
// ...or subscribe only to certain conditions (i.e. when `is_enabled` attribute value is `true`)
|
338
|
+
// For the list of supported operators (like `..._eq`), see JS API `MODEL.subscribe(CONFIG)` below
|
339
|
+
// subscription = LiveRecord.Model.all.Book.subscribe({where: {is_enabled_eq: true}});
|
340
|
+
|
341
|
+
// now, we can just simply add a "create" callback, to apply our own logic whenever a new Book record is streamed from the backend
|
342
|
+
LiveRecord.Model.all.Book.addCallback('after:create', function() {
|
343
|
+
// let's say you have a code here that adds this new Book on the page
|
344
|
+
// `this` refers to the Book record that has been created
|
345
|
+
console.log(this);
|
269
346
|
})
|
270
|
-
</script>
|
271
|
-
```
|
272
347
|
|
273
|
-
|
348
|
+
// you may also add callbacks specific to this `subscription`, as you may want to have multiple subscriptions. Then, see JS API `MODEL.subscribe(CONFIG)` below for information
|
274
349
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
var book1Attributes = { id: 1, title: 'Noli Me Tangere', author: 'José Rizal' }
|
279
|
-
var book2Attributes = { id: 2, title: 'ABNKKBSNPLAko?!', author: 'Bob Ong' }
|
280
|
-
|
281
|
-
// then we instantiate a Book object
|
282
|
-
var book1 = new LiveRecord.Model.all.Book(book1Attributes);
|
283
|
-
// then we push this Book object to the Book store, which then automatically subscribes them to changes in the backend
|
284
|
-
book1.create();
|
285
|
-
|
286
|
-
var book2 = new LiveRecord.Model.all.Book(book2Attributes);
|
287
|
-
book2.create();
|
288
|
-
|
289
|
-
// you can also add Instance callbacks specific only to this Object (supported callbacks are the same as the Model callbacks)
|
290
|
-
book2.addCallback('after:update', function() {
|
291
|
-
// do something when book2 has been updated after syncing
|
292
|
-
})
|
293
|
-
```
|
350
|
+
// then unsubscribe, as you wish
|
351
|
+
LiveRecord.Model.all.Book.unsubscribe(subscription);
|
352
|
+
```
|
294
353
|
|
354
|
+
### Ransack Search Queries (Optional)
|
355
|
+
|
356
|
+
* If you need more complex queries to pass into the `.subscribe(where: { ... })` above, [ransack](https://github.com/activerecord-hackery/ransack) gem is supported.
|
357
|
+
* For example you can then do:
|
358
|
+
```js
|
359
|
+
// querying upon the `belongs_to :user`
|
360
|
+
subscription = LiveRecord.Model.all.Book.subscribe({where: {user_is_admin_eq: true, is_enabled: true}});
|
361
|
+
|
362
|
+
// or querying "OR" conditions
|
363
|
+
subscription = LiveRecord.Model.all.Book.subscribe({where: {title_eq: 'I am Batman', content_eq: 'I am Batman', m: 'or'}});
|
364
|
+
```
|
365
|
+
|
366
|
+
#### Model File (w/ Ransack) Example
|
367
|
+
|
368
|
+
```ruby
|
369
|
+
# app/models/book.rb
|
370
|
+
class Book < ApplicationRecord
|
371
|
+
include LiveRecord::Model::Callbacks
|
372
|
+
has_many :live_record_updates, as: :recordable
|
373
|
+
|
374
|
+
def self.live_record_whitelisted_attributes(book, current_user)
|
375
|
+
[:title, :is_enabled]
|
376
|
+
end
|
377
|
+
|
378
|
+
private
|
379
|
+
|
380
|
+
# see ransack gem for more details: https://github.com/activerecord-hackery/ransack#authorization-whitelistingblacklisting
|
381
|
+
# you can write your own columns here, but you may just simply allow ALL COLUMNS to be searchable, because the `live_record_whitelisted_attributes` method above will be also called anyway, and therefore just simply handle whitelisting there.
|
382
|
+
# therefore you can actually remove the whole `self.ransackable_attributes` method below
|
383
|
+
|
384
|
+
## LiveRecord passes the `current_user` into `auth_object`, so you can access `current_user` inside below
|
385
|
+
# def self.ransackable_attributes(auth_object = nil)
|
386
|
+
# column_names + _ransackers.keys
|
387
|
+
# end
|
388
|
+
end
|
389
|
+
```
|
390
|
+
|
391
|
+
### Reconnection Streaming (when client got disconnected)
|
392
|
+
|
393
|
+
* Only requirement is that you should have a `created_at` attribute on your Models, which by default should already be there. However, to speed up queries, I highly suggest to add index on `created_at` with the following
|
394
|
+
|
395
|
+
```bash
|
396
|
+
# this will create a file under db/migrate folder, then edit that file (see the ruby code below)
|
397
|
+
rails generate migration add_created_at_index_to_MODELNAME
|
398
|
+
```
|
399
|
+
|
400
|
+
```ruby
|
401
|
+
# db/migrate/2017**********_add_created_at_index_to_MODELNAME.rb
|
402
|
+
class AddCreatedAtIndexToMODELNAME < ActiveRecord::Migration[5.0] # or 5.1, etc
|
403
|
+
def change
|
404
|
+
add_index :TABLENAME, :created_at
|
405
|
+
end
|
406
|
+
end
|
407
|
+
```
|
295
408
|
|
296
409
|
## Plugins
|
297
410
|
|
@@ -325,7 +438,7 @@
|
|
325
438
|
|
326
439
|
## JS API
|
327
440
|
|
328
|
-
`LiveRecord.Model.create(CONFIG)`
|
441
|
+
### `LiveRecord.Model.create(CONFIG)`
|
329
442
|
* `CONFIG` (Object)
|
330
443
|
* `modelName`: (String, Required)
|
331
444
|
* `callbacks`: (Object)
|
@@ -340,54 +453,97 @@
|
|
340
453
|
* `after:destroy`: (Array of functions)
|
341
454
|
* `plugins`: (Object)
|
342
455
|
* `LiveDOM`: (Boolean)
|
343
|
-
*
|
456
|
+
* creates a `MODEL` and stores it into `LiveRecord.Model.all` array
|
457
|
+
* returns the newly created `MODEL`
|
344
458
|
|
345
|
-
`
|
459
|
+
### `MODEL`.subscribe(CONFIG)
|
460
|
+
* `CONFIG` (Object, Optional)
|
461
|
+
* `where`: (Object)
|
462
|
+
* `ATTRIBUTENAME_OPERATOR`: (Any Type)
|
463
|
+
* `callbacks`: (Object)
|
464
|
+
* `on:connect`: (function Object)
|
465
|
+
* `on:disconnect`: (function Object)
|
466
|
+
* `before:create`: (function Object)
|
467
|
+
* `after:create`: (function Object)
|
468
|
+
* subscribes to the `PublicationsChannel`, which then automatically receives new records from the backend.
|
469
|
+
* you can also pass in `callbacks` (see above). These callbacks is only applicable to this subscription, and is independent of the Model and Instance callbacks.
|
470
|
+
* `ATTRIBUTENAME_OPERATOR` means something like (for example): `is_enabled_eq`, where `is_enabled` is the `ATTRIBUTENAME` and `eq` is the `OPERATOR`.
|
471
|
+
* you can have as many `ATTRIBUTENAME_OPERATOR` as you like, but keep in mind that the logic applied to them is "AND", and not "OR". For "OR" conditions, use `ransack`
|
472
|
+
|
473
|
+
#### List of Default Supported Query Operators
|
474
|
+
|
475
|
+
> the following list only applies if you are NOT using the `ransack` gem. If you need more complex queries, `ransack` is supported and so see Setup's step 9 above
|
476
|
+
|
477
|
+
* `eq` equals; i.e. `is_enabled_eq: true`
|
478
|
+
* `not_eq` not equals; i.e. `title_not_eq: 'Harry Potter'`
|
479
|
+
* `lt` less than; i.e. `created_at_lt: '2017-12-291T13:47:59.238Z'`
|
480
|
+
* `lteq` less than or equal to; i.e. `created_at_lteq: '2017-12-291T13:47:59.238Z'`
|
481
|
+
* `gt` greater than; i.e. `created_at_gt: '2017-12-291T13:47:59.238Z'`
|
482
|
+
* `gteq` greater than or equal to; i.e. `created_at_gteq: '2017-12-291T13:47:59.238Z'`
|
483
|
+
* `in` in Array; i.e. `id_in: [2, 56, 19, 68]`
|
484
|
+
* `not_in` in Array; i.e. `id_not_in: [2, 56, 19, 68]`
|
485
|
+
|
486
|
+
### `MODEL`.unsubscribe(SUBSCRIPTION)
|
487
|
+
* unsubscribes to the `PublicationsChannel`, thereby will not be receiving new records anymore.
|
488
|
+
|
489
|
+
### `new LiveRecord.Model.all.MODELNAME(ATTRIBUTES)`
|
346
490
|
* `ATTRIBUTES` (Object)
|
347
491
|
* returns a `MODELINSTANCE` of the the Model having `ATTRIBUTES` attributes
|
348
492
|
|
349
|
-
`MODELINSTANCE.modelName()`
|
493
|
+
### `MODELINSTANCE.modelName()`
|
350
494
|
* returns the model name (i.e. 'Book')
|
351
495
|
|
352
|
-
`MODELINSTANCE.attributes`
|
496
|
+
### `MODELINSTANCE.attributes`
|
353
497
|
* the attributes object
|
354
498
|
|
355
|
-
`MODELINSTANCE.ATTRIBUTENAME()`
|
499
|
+
### `MODELINSTANCE.ATTRIBUTENAME()`
|
356
500
|
* returns the attribute value of corresponding to `ATTRIBUTENAME`. (i.e. `bookInstance.id()`, `bookInstance.created_at()`)
|
357
501
|
|
358
|
-
`MODELINSTANCE.subscribe()`
|
502
|
+
### `MODELINSTANCE.subscribe()`
|
359
503
|
* 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.
|
360
504
|
* returns the `subscription` object (the ActionCable subscription object itself)
|
361
505
|
|
362
|
-
`MODELINSTANCE.
|
506
|
+
### `MODELINSTANCE.unsubscribe()`
|
507
|
+
* unsubscribes to the `LiveRecordChannel`, thereby will not be receiving changes (updates/destroy) anymore.
|
508
|
+
|
509
|
+
### `MODELINSTANCE.isSubscribed()`
|
363
510
|
* returns `true` or `false` accordingly if the instance is subscribed
|
364
511
|
|
365
|
-
`MODELINSTANCE.subscription`
|
512
|
+
### `MODELINSTANCE.subscription`
|
366
513
|
* the `subscription` object (the ActionCable subscription object itself)
|
367
514
|
|
368
|
-
`MODELINSTANCE.create()`
|
515
|
+
### `MODELINSTANCE.create()`
|
369
516
|
* stores the instance to the store, and then `subscribe()` to the `LiveRecordChannel` for syncing
|
370
517
|
* returns the instance
|
371
518
|
|
372
|
-
`MODELINSTANCE.update(ATTRIBUTES)`
|
519
|
+
### `MODELINSTANCE.update(ATTRIBUTES)`
|
373
520
|
* `ATTRIBUTES` (Object)
|
374
521
|
* updates the attributes of the instance
|
375
522
|
* returns the instance
|
376
523
|
|
377
|
-
`MODELINSTANCE.destroy()`
|
524
|
+
### `MODELINSTANCE.destroy()`
|
378
525
|
* removes the instance from the store, and then `unsubscribe()`
|
379
526
|
* returns the instance
|
380
527
|
|
381
|
-
`MODELINSTANCE.addCallback(CALLBACKKEY, CALLBACKFUNCTION)`
|
528
|
+
### `MODELINSTANCE.addCallback(CALLBACKKEY, CALLBACKFUNCTION)`
|
382
529
|
* `CALLBACKKEY` (String) see supported callbacks above
|
383
530
|
* `CALLBACKFUNCTION` (function Object)
|
384
531
|
* returns the function Object if successfuly added, else returns `false` if callback already added
|
385
532
|
|
386
|
-
`MODELINSTANCE.removeCallback(CALLBACKKEY, CALLBACKFUNCTION)`
|
533
|
+
### `MODELINSTANCE.removeCallback(CALLBACKKEY, CALLBACKFUNCTION)`
|
387
534
|
* `CALLBACKKEY` (String) see supported callbacks above
|
388
535
|
* `CALLBACKFUNCTION` (function Object) the function callback that will be removed
|
389
536
|
* returns the function Object if successfully removed, else returns `false` if callback is already removed
|
390
537
|
|
538
|
+
## FAQ
|
539
|
+
* How to remove the view templates being overriden by LiveRecord when generating a controller or scaffold?
|
540
|
+
* amongst other things, `rails generate live_record:install` will override the default scaffold view templates: **show.html.erb** and **index.html.erb**; to revert back, just simply delete the following files (though you'll need to manually update or regenerate the view files that were already generated prior to deleting to the following files):
|
541
|
+
* **lib/templates/erb/scaffold/index.html.erb**
|
542
|
+
* **lib/templates/erb/scaffold/show.html.erb**
|
543
|
+
|
544
|
+
* How to support more complex queries / "where" conditions when subscribing to new records creation?
|
545
|
+
* Please refer to [JS API's MODEL.subscribe(CONFIG) above ](#modelsubscribeconfig)
|
546
|
+
|
391
547
|
## TODOs
|
392
548
|
* Change `feature` specs into `system` specs after [this rspec-rails pull request](https://github.com/rspec/rspec-rails/pull/1813) gets merged.
|
393
549
|
|
@@ -395,4 +551,9 @@
|
|
395
551
|
* pull requests and forks are very much welcomed! :) Let me know if you find any bug! Thanks
|
396
552
|
|
397
553
|
## License
|
398
|
-
* MIT
|
554
|
+
* MIT
|
555
|
+
|
556
|
+
## Changelog
|
557
|
+
* 0.2
|
558
|
+
* Ability to subscribe to new records (supports lost connection auto-restreaming)
|
559
|
+
* See [9th step of Setup above](#setup)
|