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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -2
  3. data/{lib/live_record/.rspec → .rspec} +0 -0
  4. data/.travis.yml +0 -1
  5. data/Gemfile +3 -1
  6. data/README.md +363 -202
  7. data/Rakefile +5 -0
  8. data/app/assets/javascripts/live_record.coffee +4 -0
  9. data/app/assets/javascripts/live_record/helpers.coffee +4 -0
  10. data/app/assets/javascripts/live_record/helpers/load_records.coffee +13 -7
  11. data/app/assets/javascripts/live_record/helpers/spaceship.coffee +13 -0
  12. data/app/assets/javascripts/live_record/model.coffee +4 -0
  13. data/app/assets/javascripts/live_record/model/create.coffee +96 -27
  14. data/app/assets/javascripts/live_record/plugins.coffee +3 -0
  15. data/app/assets/javascripts/live_record/plugins/live_dom.coffee +5 -19
  16. data/app/assets/javascripts/live_record/plugins/live_dom/apply_to_model.coffee +17 -0
  17. data/app/channels/live_record/base_channel.rb +41 -0
  18. data/app/channels/live_record/changes_channel.rb +59 -0
  19. data/app/channels/live_record/publications_channel.rb +134 -0
  20. data/{lib/live_record/config.ru → config.ru} +0 -0
  21. data/lib/live_record.rb +2 -0
  22. data/lib/live_record/action_view_extensions/view_helper.rb +22 -0
  23. data/lib/live_record/configure.rb +19 -0
  24. data/lib/live_record/generators/install_generator.rb +9 -4
  25. data/lib/live_record/generators/templates/create_live_record_updates.rb +1 -1
  26. data/lib/live_record/generators/templates/index.html.erb +1 -0
  27. data/lib/live_record/generators/templates/model.rb.rb +4 -4
  28. data/lib/live_record/model/callbacks.rb +8 -2
  29. data/lib/live_record/version.rb +1 -1
  30. data/live_record.gemspec +2 -2
  31. data/{lib/live_record/spec → spec}/factories/posts.rb +0 -0
  32. data/spec/features/live_record_syncing_spec.rb +184 -0
  33. data/spec/helpers/wait.rb +19 -0
  34. data/{lib/live_record/spec → spec}/internal/app/assets/config/manifest.js +0 -0
  35. data/{lib/live_record/spec → spec}/internal/app/assets/javascripts/application.js +0 -0
  36. data/{lib/live_record/spec → spec}/internal/app/assets/javascripts/cable.js +0 -0
  37. data/spec/internal/app/assets/javascripts/posts.coffee +8 -0
  38. data/{lib/live_record/spec → spec}/internal/app/channels/application_cable/channel.rb +0 -0
  39. data/{lib/live_record/spec → spec}/internal/app/channels/application_cable/connection.rb +4 -4
  40. data/{lib/live_record/spec → spec}/internal/app/controllers/application_controller.rb +0 -0
  41. data/{lib/live_record/spec → spec}/internal/app/controllers/posts_controller.rb +1 -0
  42. data/{lib/live_record/spec → spec}/internal/app/models/application_record.rb +0 -0
  43. data/{lib/live_record/spec → spec}/internal/app/models/live_record_update.rb +0 -0
  44. data/spec/internal/app/models/post.rb +11 -0
  45. data/{lib/live_record/spec → spec}/internal/app/views/layouts/application.html.erb +0 -0
  46. data/{lib/live_record/spec → spec}/internal/app/views/posts/_form.html.erb +0 -0
  47. data/{lib/live_record/spec → spec}/internal/app/views/posts/_post.json.jbuilder +0 -0
  48. data/{lib/live_record/spec → spec}/internal/app/views/posts/edit.html.erb +0 -0
  49. data/{lib/live_record/spec → spec}/internal/app/views/posts/index.html.erb +6 -5
  50. data/{lib/live_record/spec → spec}/internal/app/views/posts/index.json.jbuilder +0 -0
  51. data/{lib/live_record/spec → spec}/internal/app/views/posts/new.html.erb +0 -0
  52. data/{lib/live_record/spec → spec}/internal/app/views/posts/show.html.erb +0 -0
  53. data/{lib/live_record/spec → spec}/internal/app/views/posts/show.json.jbuilder +0 -0
  54. data/{lib/live_record/spec → spec}/internal/config/cable.yml +0 -0
  55. data/{lib/live_record/spec → spec}/internal/config/database.yml +0 -0
  56. data/{lib/live_record/spec → spec}/internal/config/routes.rb +0 -0
  57. data/{lib/live_record/spec → spec}/internal/db/schema.rb +2 -0
  58. data/{lib/live_record/spec → spec}/internal/public/favicon.ico +0 -0
  59. data/{lib/live_record/spec → spec}/rails_helper.rb +4 -2
  60. data/{lib/live_record/spec → spec}/spec_helper.rb +0 -0
  61. metadata +64 -56
  62. data/app/assets/javascripts/live_record.js +0 -4
  63. data/app/assets/javascripts/live_record/helpers.js +0 -4
  64. data/app/assets/javascripts/live_record/model.js +0 -4
  65. data/app/assets/javascripts/live_record/plugins.js +0 -3
  66. data/lib/live_record/channel/implement.rb +0 -100
  67. data/lib/live_record/spec/features/live_record_syncing_spec.rb +0 -60
  68. data/lib/live_record/spec/internal/app/assets/javascripts/posts.coffee +0 -14
  69. data/lib/live_record/spec/internal/app/channels/live_record_channel.rb +0 -4
  70. 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: a996d306c6802ca85328ab7bfc6d2c3e10e2d1de
4
- data.tar.gz: af1c2f69850844c08823ac0039973408bad1f38f
3
+ metadata.gz: 68e46da08f168efb9e1ebfb51f075548aaaa42cd
4
+ data.tar.gz: fbb81c2b262d59fa5b05e7a8b1a84bb20962538f
5
5
  SHA512:
6
- metadata.gz: d8feaa50d1fd4f9a4c549a1d30f817e8e75ed95ba9cf2fc9eeb1d8353c4706eb6ee3b831db54799d2da2acbd6a75709b640fa3b7f348580c988667b972323e40
7
- data.tar.gz: 131c98a95177c115e223d58bcd609dc2621515f445593b9ec41d07e279c80aaffe5efd3521517aabcf8deb71a8ed83a2a53b0b9bc4439f8568fbbed5ee187251
6
+ metadata.gz: 2c3a0ffcac47a14e301671222403391380ece8577cf2a62ec1477b604155a31fb660b5fcc2e3106ebf2e94245c77e38b32aaf83d7c5e0d24ca1617fdd19370e3
7
+ data.tar.gz: 95949617075641208bbf35ace36222c43f4d11bb415110ea1da45ef05281191b28dc80e4136dadf9ff71f6f4f4836ae14b102403ae2e53973e49fbdae5992338
data/.gitignore CHANGED
@@ -1,8 +1,8 @@
1
1
  /.bundle
2
2
  *.sqlite
3
3
  *.sqlite-journal
4
- /lib/live_record/spec/internal/log/*
5
- /lib/live_record/spec/internal/tmp/*
4
+ /spec/internal/log/*
5
+ /spec/internal/tmp/*
6
6
  .byebug_history
7
7
  *.gem
8
8
  Gemfile.lock
File without changes
data/.travis.yml CHANGED
@@ -9,4 +9,3 @@ rvm:
9
9
  - 2.4.1
10
10
  before_install:
11
11
  - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost &
12
- script: cd lib/live_record && bundle exec rspec
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
- * Auto-updates DOM elements mapped to a record attribute, from changes. **(Optional LiveDOM Plugin)**
7
- * Automatically resyncs after client-side reconnection.
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
- * on the client-side:
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
- // instantiate a Book object
26
- var book = new LiveRecord.Model.all.Book({
27
- id: 1,
28
- title: 'Harry Potter',
29
- author: 'J. K. Rowling',
30
- created_at: '2017-08-02T12:39:49.238Z',
31
- updated_at: '2017-08-02T12:39:49.238Z'
32
- });
33
- // store this Book object into the JS store
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
- // you can add a callback that will be invoked whenever the Book object has been updated (see all supported callbacks further below)
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
- * Add the following to your `Gemfile`:
112
+ 1. Add the following to your `Gemfile`:
81
113
 
82
- ```ruby
83
- gem 'live_record', '~> 0.1.2'
84
- ```
114
+ ```ruby
115
+ gem 'live_record', '~> 0.2.0'
116
+ ```
85
117
 
86
- * Run:
118
+ 2. Run:
87
119
 
88
- ```bash
89
- bundle install
90
- ```
120
+ ```bash
121
+ bundle install
122
+ ```
91
123
 
92
- * Install by running:
124
+ 3. Install by running:
93
125
 
94
- ```bash
95
- rails generate live_record:install
96
- ```
126
+ ```bash
127
+ rails generate live_record:install
128
+ ```
97
129
 
98
- > `rails generate live_record:install --live_dom=false` if you do not need the `LiveDOM` plugin; `--live_dom=true` by default
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
- * Run migration to create the `live_record_updates` table, which is going to be used for client reconnection resyncing:
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
- * Update your **app/channels/application_cable/connection.rb**, and add `current_user` method, unless you already have it:
138
+ 5. Update your **app/channels/application_cable/connection.rb**, and add `current_user` method, unless you already have it:
107
139
 
108
- ```ruby
109
- module ApplicationCable
110
- class Connection < ActionCable::Connection::Base
111
- identified_by :current_user
140
+ ```ruby
141
+ module ApplicationCable
142
+ class Connection < ActionCable::Connection::Base
143
+ identified_by :current_user
112
144
 
113
- def current_user
114
- # write something here if you have a current_user, or you may just leave this blank. Example below when using `devise` gem:
115
- # User.find_by(id: cookies.signed[:user_id])
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
- end
119
- ```
151
+ ```
120
152
 
121
- * Update your **model** files (only those you would want to be synced), and insert the following public method:
153
+ 6. Update your **model** files (only those you would want to be synced), and insert the following public method:
122
154
 
123
- > automatically updated if you use Rails scaffold or model generator
155
+ > automatically updated if you use Rails scaffold or model generator
124
156
 
125
- ### Example 1 - Simple Usage
157
+ ### Example 1 - Simple Usage
126
158
 
127
- ```ruby
128
- # app/models/book.rb (example 1)
129
- class Book < ApplicationRecord
130
- def self.live_record_whitelisted_attributes(book, current_user)
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
- ```ruby
141
- # app/models/book.rb (example 1)
142
- class Book < ApplicationRecord
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
- end
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
- * For each Model you want to sync, insert the following in your Javascript files.
193
+ 7. For each Model you want to sync, insert the following in your Javascript files.
158
194
 
159
- > automatically updated if you use Rails scaffold or controller generator
195
+ > automatically updated if you use Rails scaffold or controller generator
160
196
 
161
- ### Example 1 - Model
197
+ ### Example 1 - Model
162
198
 
163
- ```js
164
- // app/assets/javascripts/books.js
165
- LiveRecord.Model.create(
166
- {
167
- modelName: 'Book' // should match the Rails model name
168
- plugins: {
169
- LiveDOM: true // remove this if you do not need `LiveDOM`
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
- ### Example 2 - Model + Callbacks
176
-
177
- ```js
178
- // app/assets/javascripts/books.js
179
- LiveRecord.Model.create(
180
- {
181
- modelName: 'Book',
182
- callbacks: {
183
- 'on:connect': [
184
- function() {
185
- console.log(this); // `this` refers to the current `Book` record that has just connected for syncing
186
- }
187
- ],
188
- 'after:update': [
189
- function() {
190
- console.log(this); // `this` refers to the current `Book` record that has just been updated with changes synced from the backend
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
- #### Model Callbacks supported:
199
- * `on:connect`
200
- * `on:disconnect`
201
- * `on:response_error`
202
- * `before:create`
203
- * `after:create`
204
- * `before:update`
205
- * `after:update`
206
- * `before:destroy`
207
- * `after:destroy`
208
-
209
- > Each callback should map to an array of functions
210
-
211
- * `on:response_error` supports a function argument: The "Error Code". i.e.
212
-
213
- ### Example 3 - Handling Response Error
214
-
215
- ```js
216
- LiveRecord.Model.create(
217
- {
218
- modelName: 'Book',
219
- callbacks: {
220
- 'on:response_error': [
221
- function(errorCode) {
222
- console.log(errorCode); // errorCode is a string, representing the type of error. See Response Error Codes below:
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
- #### Response Error Codes:
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
- * Load the records into the JS Model-store through JSON REST (i.e.):
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
- ### Example 1 - Using Default Loader (Requires JQuery)
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
- > Your controller must also support responding with JSON in addition to HTML. If you used scaffold or controller generator, this should already work immediately.
322
+ var book2 = new LiveRecord.Model.all.Book(book2Attributes);
323
+ book2.create();
239
324
 
240
- ```html
241
- <!-- app/views/books/index.html.erb -->
242
- <script>
243
- // `loadRecords` asynchronously loads all records (using the current URL) to the store, through a JSON AJAX request.
244
- // in this example, `loadRecords` will load JSON from the current URL which is /books
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
- ```html
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
- ```html
258
- <!-- app/views/posts/index.html.erb -->
259
- <script>
260
- // You may also pass in a callback for synchronous logic
261
- LiveRecord.helpers.loadRecords({
262
- modelName: 'Book',
263
- onLoad: function(records) {
264
- // ...
265
- },
266
- onError: function(jqxhr, textStatus, error) {
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
- ### Example 2 - Using Custom Loader
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
- ```js
276
- // do something here that will fetch Book record attributes...
277
- // as an example, say you already have the following attributes:
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
- * returns the newly create `MODEL`
456
+ * creates a `MODEL` and stores it into `LiveRecord.Model.all` array
457
+ * returns the newly created `MODEL`
344
458
 
345
- `new LiveRecord.Model.all.MODELNAME(ATTRIBUTES)`
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.isSubscribed()`
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)