entangled 0.0.16 → 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9c1338c6837e0167bc7fab4bf7f0ca67a2e572ba
4
- data.tar.gz: 27dc18b0cb3570ae172c603baca469a8958ead45
3
+ metadata.gz: 620c93edc4a4892c41fe62cfd60cd448a245621e
4
+ data.tar.gz: d0cde74b9d171662e6bb716349a80eedcf943441
5
5
  SHA512:
6
- metadata.gz: 89baa6f21fd914f7e8f6664a5712ac6af91891bb210b28be642c0b5acd36f6bf287e70a7a39888f2cf8e17397ed0bd3d1de06b66ea8bded1982d08946c7273e9
7
- data.tar.gz: edcafa59d08bfedb94741745f7a6fb8365ff87a68fd8a83ac2409876bdd8601e0fd0bc9a9be40730a1a3ee5f035a173c2fc1d6eb4f8b4ae82e5d919c096d0a38
6
+ metadata.gz: 24ee4a89da4c1ff4dd462ea2b34f335cb9c05aea74488a6682e6ab6959e0388b3894262ff4adde9f15896c039f8de9a9015d0cce61e794e7a91fa322771493d0
7
+ data.tar.gz: 34d56b6e058a103daa410ca346e01165ff5de40eccab0c04c61c8ee5ec13029498f991106dc8b97d012fa1712123a3d93376bf0b73ba949703818143edb86747
data/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  [![Codeship Status for dchacke/entangled](https://codeship.com/projects/9fe9a790-9df7-0132-5fb8-6e77ea26735b/status?branch=master)](https://codeship.com/projects/64679)
4
4
 
5
- Services like Firebase are great because they provide real time data binding between client and server. But they come at a price: You give up control over your backend. Wouldn't it be great to have real time functionality but still keep your beloved Rails backend? That's where Entangled comes in.
5
+ Services like Firebase are great because they provide real time data binding between client and server. But they come at a price: You give up control over your back end. Wouldn't it be great to have real time functionality but still keep your beloved Rails back end? That's where Entangled comes in.
6
6
 
7
- Entangled is a layer behind your controllers and models that pushes updates to clients subscribed to certain channels in real time. For example, if you display a list of five messages on a page, if anyone adds a sixth message, everyone who is currently looking at that page will instantly see that sixth message being added to the list.
7
+ Entangled stores and syncs data instantly across every device. It is a layer behind your controllers and models that pushes updates to all connected clients in real time. It is cross-browser compatible and even offers real time validations.
8
8
 
9
- The idea is that real time data binding should be the default, not an add-on. Entangled aims at making real time features as easy to implement as possible, while at the same time making your restful controllers thinner.
9
+ Real time data binding should be the default, not an add-on. Entangled aims at making real time features as easy to implement as possible, while at the same time making your restful controllers thinner. All this without having to give up control over your back end.
10
10
 
11
11
  ## Installation
12
12
  Add this line to your application's Gemfile:
@@ -104,13 +104,14 @@ class MessagesController < ApplicationController
104
104
 
105
105
  def create
106
106
  broadcast do
107
- Message.create(message_params)
107
+ @message = Message.create(message_params)
108
108
  end
109
109
  end
110
110
 
111
111
  def update
112
112
  broadcast do
113
- Message.find(params[:id]).update(message_params)
113
+ @message = Message.find(params[:id])
114
+ @message.update(message_params)
114
115
  end
115
116
  end
116
117
 
@@ -131,8 +132,7 @@ Note the following:
131
132
 
132
133
  - All methods are wrapped in a new `broadcast` block needed to send messages to connected clients
133
134
  - The `index` method will expect an instance variable with the same name as your controller in the plural form (e.g. `@messages` in a `MessagesController`)
134
- - The `show` method will expect an instance variable with the singular name of your controller (e.g. `@message` in a `MessagesController`)
135
- - Instance variables only need to be assigned in `index` and `show` since these are the only methods that should be concerned with sending data to clients. All other methods only publish updates to the data clients are subscribed to through the callbacks added to the model, so no instance variables are needed
135
+ - The `show`, `create` and `update` methods will expect an instance variable with the singular name of your controller (e.g. `@message` in a `MessagesController`)
136
136
  - Data sent to clients arrives as stringified JSON
137
137
  - Strong parameters are expected
138
138
 
@@ -146,13 +146,13 @@ $ redis-server
146
146
 
147
147
  Otherwise the channels won't work.
148
148
 
149
- If you store your Redis instance in `$redis` or `REDIS` (e.g. in an initializer), Entangled will use that assigned instance so that you can configure Redis just like you're used to. Otherwise, Entangled will instantiate Redis itself and use its standard settings.
149
+ If you store your Redis instance in `$redis` or `REDIS` (e.g. in an initializer), Entangled will use that assigned instance so that you can configure Redis just like you're used to. Otherwise, Entangled will instantiate Redis itself and use its default settings.
150
150
 
151
151
  ### Database
152
152
  Depending on your app's settings, you might have to increase the pool size in your database.yml configuration file, since every new socket will open a new connection to your database.
153
153
 
154
154
  ## The Client
155
- You will need to configure your client to create Websockets and understand incoming requests on those sockets. If you use Angular for your frontend, you can use the Angular library from this repository. The use of Angular as counterpart of this gem is highly recommended, since its inherent two way data binding complements the real time functionality of this gem nicely.
155
+ You will need to configure your client to create Websockets and understand incoming requests on those sockets. If you use Angular for your front end, you can use the Angular library from this repository. The use of Angular as counterpart of this gem is highly recommended, since its inherent two way data binding complements the real time functionality of this gem nicely.
156
156
 
157
157
  ### Installation
158
158
  You can either download or reference the file `entangled.js` from this repository, or simply install it with Bower:
@@ -174,25 +174,24 @@ Entangled is best used within Angular services. For example, consider a `Message
174
174
 
175
175
  ```javascript
176
176
  app.factory('Message', function(Entangled) {
177
- var entangled = new Entangled('ws://localhost:3000/messages');
178
-
179
- var Message = {
180
- new: function(params) {
181
- return entangled.new(params);
182
- },
183
- all: function(callback) {
184
- return entangled.all(callback);
185
- },
186
- find: function(id, callback) {
187
- return entangled.find(id, callback);
188
- }
189
- };
190
-
191
- return Message;
177
+ return new Entangled('ws://localhost:3000/messages');
192
178
  });
193
179
  ```
194
180
 
195
- In the above example, first we inject Entangled into our service, then instantiate a new Entangled object. The Entangled object takes one argument when instantiated: the URL of your resource's index route (in this case, `/messages`). Then we add helper methods to our service. Note that the socket URL looks just like a standard restful URL with http, except that the protocol part has been switched with `ws` to use the websocket protocol. Also note that you need to use `wss` instead if you want to use SSL.
181
+ In the above example, first we inject Entangled into our service, then instantiate a new Entangled object and return it. The Entangled object takes one argument when instantiated: the URL of your resource's index action (in this case, `/messages`). Note that the socket URL looks just like a standard restful URL with http, except that the protocol part has been switched with `ws` to use the websocket protocol. Also note that you need to use `wss` instead if you want to use SSL.
182
+
183
+ The Entangled service come with these functions:
184
+
185
+ - `new(params)`
186
+ - `create(params, callback)`
187
+ - `find(id, callback)`
188
+ - `all(callback)`
189
+
190
+ ...and the following functions on returned objects:
191
+
192
+ - `$save(callback)`
193
+ - `$update(params, callback)`
194
+ - `$destroy(callback)`
196
195
 
197
196
  In your controller, you could then inject that `Message` service and use it like so:
198
197
 
@@ -202,20 +201,21 @@ In your controller, you could then inject that `Message` service and use it like
202
201
  // set some default values
203
202
  $scope.message = Message.new();
204
203
 
204
+ // To instantiate and save a message in one go
205
+ Message.create({ body: 'text' }, function(message) {
206
+ $scope.$apply(function() {
207
+ $scope.message = message;
208
+ });
209
+ });
210
+
205
211
  // To retrieve a specific message from the server
206
212
  // with id 1 and subscribe to its channel
207
- Message.find(1, function() {
213
+ Message.find(1, function(message) {
208
214
  $scope.$apply(function() {
209
215
  $scope.message = message;
210
216
  });
211
217
  });
212
218
 
213
- // To create a new or update an existing message
214
- $scope.message.$save();
215
-
216
- // To destroy a message
217
- $scope.message.$destroy();
218
-
219
219
  // To retrieve all messages from the server and
220
220
  // subscribe to the collection's channel
221
221
  Message.all(function(messages) {
@@ -223,14 +223,83 @@ Message.all(function(messages) {
223
223
  $scope.messages = messages;
224
224
  });
225
225
  });
226
+
227
+ // To store a newly instantiated or update an existing message.
228
+ // If saved successfully, $scope.message is updated in place
229
+ // with the attributes id, created_at and updated_at
230
+ $scope.message.body = 'new body';
231
+ $scope.message.$save(function() {
232
+ // Do stuff after save
233
+ });
234
+
235
+ // To update a newly instantiated or existing message in place.
236
+ // If updated successfully, $scope.message is updated in place
237
+ // with the attributes id, created_at and updated_at
238
+ $scope.message.$update({ body: 'new body' }, function() {
239
+ // Do stuff after update
240
+ });
241
+
242
+ // To destroy a message
243
+ $scope.message.$destroy(function() {
244
+ // Do stuff after destroy
245
+ });
226
246
  ```
227
247
 
228
- `$save()`, `$destroy()`, `find()` and `all()` will interact with your server's controllers in real time.
248
+ All functions above will interact with your server's controllers in real time.
229
249
 
230
250
  If data in your server's database changes, so will your scope variables - in real time, for all connected clients.
231
251
 
252
+ ### Available Functions
253
+ A number of functions is attached to Entangled JavaScript objects. They basically mimic ActiveRecord's behavior in the back end to make the database more accessible in the front end.
254
+
255
+ #### Validations
256
+ Objects from the Entangled service automatically receive ActiveRecord's error messages from your model when you `$save()`. An additional property called `errors` containing the error messages is available, formatted the same way you're used to from calling `.errors` on a model in Rails.
257
+
258
+ For example, consider the following scenario:
259
+
260
+ ```ruby
261
+ # Message model (Rails)
262
+ validates :body, presence: true
263
+ ```
264
+
265
+ ```javascript
266
+ // Controller (Angular)
267
+ $scope.message.$save(function() {
268
+ console.log($scope.message.errors);
269
+ // => { body: ["can't be blank"] }
270
+ });
271
+ ```
272
+
273
+ You could then display these error messages to your users.
274
+
275
+ To check if a resource is valid, you can use `$valid()` and `$invalid()`. Both functions return booleans. For example:
276
+
277
+ ```javascript
278
+ $scope.message.$save(function() {
279
+ // Check if record has no errors
280
+ if ($scope.message.$valid()) { // similar to ActiveRecord's .valid?
281
+ alert('Yay!');
282
+ }
283
+
284
+ // Check if record errors
285
+ if ($scope.message.$invalid()) { // similar to ActiveRecord's .invalid?
286
+ alert('Nay!');
287
+ }
288
+ });
289
+ ```
290
+
291
+ Note that `$valid()` and `$invalid()` should only be used after $saving a resource, i.e. in the callback of `$save`, since they don't actually invoke server side validations. They only check if a resource contains errors.
292
+
293
+ #### Persistence
294
+ Just like with ActiveRecord's `persisted?` method, you can use `$persisted()` on an object to check if it was successfully stored in the database.
295
+
296
+ ```javascript
297
+ $scope.message.$persisted();
298
+ // => true or false
299
+ ```
300
+
232
301
  ## Planning Your Infrastructure
233
- This gem is best used for Rails apps that serve as APIs only and are not concerned with rendering views. A frontend separate from your Rails app, such as Angular with Grunt, is recommended.
302
+ This gem is best used for Rails apps that serve as APIs only and are not concerned with rendering views, since Entangled controllers cannot render views. A front end separate from your Rails app is recommended, either in your Rails app's public directory, or a separate front end app altogether.
234
303
 
235
304
  ## Limitations
236
305
  The gem rely's heavily on convention over configuration and currently only works with restful style controllers as shown above. More customization will be available soon.
@@ -240,12 +309,13 @@ The gem rely's heavily on convention over configuration and currently only works
240
309
  2. Run `$ bundle install` in the root of the repo
241
310
  3. Run `$ bower install` and `$ npm install` in spec/dummy/public
242
311
  4. The back end example app resides in spec/dummy; you can run `rails` and `rake` commands in there if you prefix them with `bin/`, i.e. `$ bin/rails s` or `$ bin/rake db:schema:load`; run your tests in the root of the repo by running `$ rspec`
243
- 5. The front end example app resides in spec/dummy/public. To look at it in your browser, cd into spec/dummy/public and run `$ bin/rails s`. Tests for this part of the app can be located under spec/dummy/public/test and are written with Jasmine. To run the tests, first run `$ bin/rails -e test` to start up the server in test mode, and then run `$ grunt test` in a new terminal tab. It's important to remember that changes you make to the server will not take effect until you restart the server since you're running it in the test environment!
244
- 6. Write your tests
245
- 7. Write your feature to make the tests pass
246
- 8. Stage and commit your changes
247
- 9. Push to a new feature branch in your repo
248
- 10. Send me a pull request!
312
+ 5. The front end example app resides in spec/dummy/public. To look at it in your browser, cd into spec/dummy/public and run `$ bin/rails s`. Tests for this part of the app can be located under spec/dummy/public/test and are written with Jasmine. To run the tests, first run `$ bin/rails -e test` to start up the server in test mode, and then run `$ grunt test` in a new terminal tab. It's important to remember that changes you make to the server will not take effect until you restart the server since you're running it in the test environment! Also remember to prepare the test database by running `$ bin/rake db:test:prepare`
313
+ 6. The Entangled Angular service resides in spec/dummy/public/app/entangled/entangled.js. This is where you can make changes to the service; a copy of it, living in /entangled.js at the root of the repo, should be kept in sync for it to be available with Bower, so it's best if you replace this file with the one from the dummy app should have made any changes to the latter
314
+ 7. Write your tests
315
+ 8. Write your feature to make the tests pass
316
+ 9. Stage and commit your changes
317
+ 10. Push to a new feature branch in your repo
318
+ 11. Send me a pull request!
249
319
 
250
320
  ## Credits
251
321
  Thanks to [Ilias Tsangaris](https://github.com/iliastsangaris) for inspiring the name "Entanglement" based on [Quantum Entanglement](http://en.wikipedia.org/wiki/Quantum_entanglement) where pairs or groups of particles always react to changes as a whole, i.e. changes to one particle will result in immediate change of all particles in the group.
data/bower.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entangled",
3
- "version": "0.0.1",
3
+ "version": "0.0.6",
4
4
  "authors": [
5
5
  "Dennis Charles Hackethal <dennis.hackethal@gmail.com>"
6
6
  ],
data/entangled.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
 
16
16
  s.files = `git ls-files -z`.split("\x0")
17
17
  s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- s.test_files = Dir["spec/**/*"]
18
+ s.test_files = `git ls-files -z spec`.split("\x0")
19
19
  s.require_paths = ["lib"]
20
20
 
21
21
  s.add_development_dependency 'bundler', '~> 1.7'
data/entangled.js CHANGED
@@ -26,43 +26,83 @@ angular.module('entangled', [])
26
26
  // an existing, depending on whether or not
27
27
  // an id is present.
28
28
  Resource.prototype.$save = function(callback) {
29
- console.log('Saving...');
30
-
31
29
  var that = this;
32
- console.log(that);
33
30
 
34
31
  if (this.id) {
35
32
  // Update
36
- console.log('Updating...');
37
33
  var socket = new WebSocket(that.webSocketUrl + '/' + that.id + '/update');
38
34
  socket.onopen = function() {
39
35
  socket.send(JSON.stringify(that));
36
+ };
40
37
 
41
- if (callback) callback();
38
+ // Receive updated resource from server
39
+ socket.onmessage = function(event) {
40
+ if (event.data) {
41
+ var data = JSON.parse(event.data);
42
42
 
43
- console.log('Updated');
43
+ // Assign/override new data (such as updated_at, etc)
44
+ if (data.resource) {
45
+ for (key in data.resource) {
46
+ that[key] = data.resource[key];
47
+ }
48
+ }
49
+ }
50
+
51
+ // Pass 'that' to callback for create
52
+ // function so that the create function
53
+ // can pass the created resource to its
54
+ // own callback; not needed for $save per se
55
+ if (callback) callback(that);
44
56
  };
45
57
  } else {
46
58
  // Create
47
- console.log('Creating...');
48
59
  var socket = new WebSocket(that.webSocketUrl + '/create');
49
60
 
61
+ // Send attributes to server
50
62
  socket.onopen = function() {
51
- console.log(socket);
52
63
  socket.send(JSON.stringify(that));
64
+ };
53
65
 
54
- if (callback) callback();
66
+ // Receive saved resource from server
67
+ socket.onmessage = function(event) {
68
+ if (event.data) {
69
+ var data = JSON.parse(event.data);
55
70
 
56
- console.log('Created');
71
+ // Assign/override new data (such as id, created_at,
72
+ // updated_at, etc)
73
+ if (data.resource) {
74
+ for (key in data.resource) {
75
+ that[key] = data.resource[key];
76
+ }
77
+ }
78
+ }
79
+
80
+ // Pass 'that' to callback for create
81
+ // function so that the create function
82
+ // can pass the created resource to its
83
+ // own callback; not needed for $save per se
84
+ if (callback) callback(that);
57
85
  };
58
86
  }
59
87
  };
60
88
 
89
+ // $update() updates a record in place
90
+ Resource.prototype.$update = function(params, callback) {
91
+ // Update object in memory
92
+ for (var key in params) {
93
+ // Skip inherent object properties
94
+ if (params.hasOwnProperty(key)) {
95
+ this[key] = params[key];
96
+ }
97
+ }
98
+
99
+ // Save object
100
+ this.$save(callback);
101
+ };
102
+
61
103
  // $destroy() will send a request to the server to
62
104
  // destroy an existing record.
63
105
  Resource.prototype.$destroy = function(callback) {
64
- console.log('From $destroy: ', this);
65
-
66
106
  var socket = new WebSocket(this.webSocketUrl + '/' + this.id + '/destroy');
67
107
  socket.onopen = function() {
68
108
  // It's fine to send an empty message since the
@@ -74,6 +114,26 @@ angular.module('entangled', [])
74
114
  };
75
115
  };
76
116
 
117
+ // $valid() checks if any errors are attached to the object
118
+ // and return false if so, false otherwise. This doesn't actually
119
+ // invoke server side validations, so it should only be used
120
+ // after calling $save() to check if the record was successfully
121
+ // stored in the database
122
+ Resource.prototype.$valid = function() {
123
+ return !(this.errors && Object.keys(this.errors).length);
124
+ };
125
+
126
+ // $invalid() returns the opposite of $valid()
127
+ Resource.prototype.$invalid = function() {
128
+ return !this.$valid();
129
+ };
130
+
131
+ // $persisted() checks if the record was successfully stored
132
+ // in the back end's database
133
+ Resource.prototype.$persisted = function() {
134
+ return !!this.id;
135
+ };
136
+
77
137
  // Resources wraps all individual Resource objects
78
138
  // in a collection.
79
139
  var Resources = function(resources, webSocketUrl) {
@@ -119,22 +179,16 @@ angular.module('entangled', [])
119
179
 
120
180
  // If the collection of Resources was sent
121
181
  if (data.resources) {
122
- console.log('Index');
123
-
124
182
  // Store retrieved Resources in property
125
183
  this.resources = new Resources(data.resources, socket.url);
126
184
  } else if (data.action) {
127
185
  if (data.action === 'create') {
128
186
  // If new Resource was created, add it to the
129
187
  // collection
130
- console.log('Created');
131
- console.log(this.resources);
132
-
133
188
  this.resources.all.push(new Resource(data.resource, socket.url));
134
189
  } else if (data.action === 'update') {
135
190
  // If an existing Resource was updated,
136
191
  // update it in the collection as well
137
- console.log('Updated');
138
192
  var index;
139
193
 
140
194
  for (var i = 0; i < this.resources.all.length; i++) {
@@ -147,7 +201,6 @@ angular.module('entangled', [])
147
201
  } else if (data.action === 'destroy') {
148
202
  // If a Resource was destroyed, remove it
149
203
  // from Resources as well
150
- console.log('Destroyed');
151
204
  var index;
152
205
 
153
206
  for (var i = 0; i < this.resources.all.length; i++) {
@@ -158,7 +211,7 @@ angular.module('entangled', [])
158
211
 
159
212
  this.resources.all.splice(index, 1);
160
213
  } else {
161
- console.log('Something else happened...');
214
+ console.log('Something else other than CRUD happened...');
162
215
  console.log(data);
163
216
  }
164
217
  }
@@ -170,6 +223,12 @@ angular.module('entangled', [])
170
223
  };
171
224
  };
172
225
 
226
+ // Instantiate and persist a record in one go
227
+ Entangled.prototype.create = function(params, callback) {
228
+ var resource = this.new(params);
229
+ resource.$save(callback);
230
+ };
231
+
173
232
  // Find an individual Resource on the server
174
233
  Entangled.prototype.find = function(id, callback) {
175
234
  var webSocketUrl = this.webSocketUrl;
@@ -180,7 +239,6 @@ angular.module('entangled', [])
180
239
  if (event.data.length) {
181
240
  // Parse message and convert to JSON
182
241
  var data = JSON.parse(event.data);
183
- console.log('Show');
184
242
 
185
243
  if (data.resource && !data.action) {
186
244
  // If the Resource was sent from the server,
@@ -190,18 +248,14 @@ angular.module('entangled', [])
190
248
  if (data.action === 'update') {
191
249
  // If the stored Resource was updated,
192
250
  // update it here as well
193
- console.log('updated!');
194
-
195
251
  this.resource = new Resource(data.resource, webSocketUrl);
196
252
  } else if (data.action === 'destroy') {
197
253
  // If the stored Resource was destroyed,
198
254
  // remove it from here as well
199
- console.log('destroyed!');
200
-
201
255
  this.resource = undefined;
202
256
  }
203
257
  } else {
204
- console.log('something else happened...');
258
+ console.log('Something else other than CRUD happened...');
205
259
  console.log(data);
206
260
  }
207
261
  }