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.
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)