live_record 0.1.2 → 0.2.0
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 +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)
|