socket_helpers 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +207 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/socket_helpers.js +267 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/socket_helpers/version.rb +3 -0
- data/lib/socket_helpers.rb +31 -0
- data/socket_helpers.gemspec +31 -0
- metadata +184 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 36c61cb6e3be55c48bd004f38081e240aa5e7600
|
4
|
+
data.tar.gz: a2941755e1dbeb379da83026e0cdfb3c631bf107
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 779830e38084229a64dd617cdcefa0e673d729d15263f52a92c34bba6a06cef83711f107c3f28e19f8a1fe529181572191e01a25772add638ba42ac472412482
|
7
|
+
data.tar.gz: 2ab2e5ea8ba1efedfa175abd4a47f9d9990bbe0b8e2e5739ebc778cb81033d8717e4e351c0dc30259ed1cab1d88b82977d79246daa6e67db5b0036d188ca77b6
|
data/.gitignore
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at maxpleaner@gmail.com. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 maxpleaner
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
# SocketHelpers
|
2
|
+
|
3
|
+
### Usage /Installation
|
4
|
+
|
5
|
+
_(these instructions can be seen implemented in the [socket_helpers_example](http://github.com/maxpleaner/socket_helpers_example) repo_ or seen [on a live site](http://socket-helpers-example.herokuapp.com)
|
6
|
+
|
7
|
+
---
|
8
|
+
|
9
|
+
####
|
10
|
+
**create rails app** `rails new App; cd App;`
|
11
|
+
|
12
|
+
**create a model** `rails g scaffold Todo content:string; rake db:migrate;`
|
13
|
+
|
14
|
+
**add gems** `gem 'socket_helpers'` and `gem 'websocket-rails'`
|
15
|
+
|
16
|
+
**add javascript requires to application.js**
|
17
|
+
|
18
|
+
- `//= require websocket_rails/main`
|
19
|
+
- `//= require socket_helpers`
|
20
|
+
|
21
|
+
**add jquery initializer** for whatever models you need websocket resources for (singular, snake case).
|
22
|
+
|
23
|
+
```javascript
|
24
|
+
$(function(){
|
25
|
+
SocketHelpers.initialize(["todo"], "http://localhost:3000/websocket")
|
26
|
+
})
|
27
|
+
```
|
28
|
+
- the default websocket url (from the websocket-rails gem) is "/websocket"
|
29
|
+
|
30
|
+
**include the controller helpers to application_controller**
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
class ApplicationController < ActionController::Base
|
34
|
+
include SocketHelpers::ControllerHelpers
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
**Remove the default scaffold routes** (`resources :todos`). This gem supports only query parameters, not path parameters. This limitation only applies to `websocket_response` endpoints. Other endpoints can use path parameters.
|
39
|
+
|
40
|
+
- i.e. parameters are never declared in the routes.rb file, but they are declared in controllers. For example, routes like `DELETE /todos/MY_TODO_ID` are not supported, but `DELETE /todos?id=MY_TODO_ID` are.
|
41
|
+
|
42
|
+
**Create a HTML-serving endpoint** `rails g controller HtmlPages root`
|
43
|
+
|
44
|
+
**Create websocket API endpoints and write routes**
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# routes.rb
|
48
|
+
get "/", to: "html_pages#root"
|
49
|
+
post "todos", to: "todos#create"
|
50
|
+
delete "todos", to: "todos#destroy"
|
51
|
+
```
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
# app/controllers/todos_controller.rb
|
55
|
+
# all the default scaffold stuff can be deleted
|
56
|
+
class TodosController < ApplicationController
|
57
|
+
def create
|
58
|
+
todo = Todo.create(todo_params)
|
59
|
+
websocket_response(todo, "create")
|
60
|
+
return false
|
61
|
+
end
|
62
|
+
def destroy
|
63
|
+
todo = Todo.find_by(id: params[:id])
|
64
|
+
todo.destroy
|
65
|
+
websocket_response(todo, "destroy")
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
def todo_params
|
69
|
+
params.permit(:content)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
73
|
+
- the first argument of `websocket_response` can be a single record or an array. _It cannot be a query_. The second can be either `create`, `destroy`, or `update` (these values hard-coded into the app. The receiver-hooks for these events are automatically created by the javascript client.
|
74
|
+
|
75
|
+
- make sure to add a 'return' or 'render' after `websocket_response` to avoid "template not found" errors.
|
76
|
+
|
77
|
+
**use the DSL for HTML** in html_pages/root.html.erb. See below for a list of HTML components available.
|
78
|
+
|
79
|
+
```html
|
80
|
+
<h3>Create todo</h3>
|
81
|
+
<form action="todos" method="POST">
|
82
|
+
<input type="text" name="content" placeholder="content">
|
83
|
+
<input type="submit" value="submit">
|
84
|
+
</form>
|
85
|
+
<div class="todo_index">
|
86
|
+
<h3>Todos</h3>
|
87
|
+
<p template>
|
88
|
+
<span template-attr="content"></span>
|
89
|
+
<form action="/todos" method="POST"
|
90
|
+
<input type="hidden" name="_method" value="DELETE"
|
91
|
+
<input type="hidden" name="id" template-attr="id"
|
92
|
+
<input type="submit" value="remove"></input>
|
93
|
+
</form>
|
94
|
+
<br>
|
95
|
+
</p>
|
96
|
+
</ul>
|
97
|
+
</div>
|
98
|
+
<div init="todo">
|
99
|
+
<%= Oj.dump [Todo.first] %>
|
100
|
+
</div>
|
101
|
+
```
|
102
|
+
|
103
|
+
- This provides working 'index, 'create', and 'destroy' websocket functionality in quite few lines of HTML, which is mainly the point of this gem. 'update' is automatic as well. When a record is added to the page, a `record-id` attribute is automatically set to `<record_class>,<id>` on the newly-added template. This is used to lookup records.
|
104
|
+
|
105
|
+
**remove CSRF token check**
|
106
|
+
|
107
|
+
comment out the `protect_from_forgery with: :exception` line in application_controller
|
108
|
+
|
109
|
+
**start rails server** `rails s;`, open [localhost:3000](http://localhost:3000)
|
110
|
+
|
111
|
+
It is a working todo-app with websockets. Try opening two browser windows at once.
|
112
|
+
|
113
|
+
---
|
114
|
+
|
115
|
+
### **List of HTML components**
|
116
|
+
|
117
|
+
- elements with a class of `<model_name>-index` become lists, with elements auto-removed and added in response to websocket events. For example, `<div class="todo-index"></div>`. These sections correspond to a single ActiveRecord class (underscore, singular i.e. `todo_list_item` for `TodoListItem`)
|
118
|
+
|
119
|
+
- inside a `<model_name>-index` element, an element with a `template` attribute becomes the template for added records. For example, `<div template></div>`
|
120
|
+
|
121
|
+
- inside a `[template]` element, the `template-attr` attribute is used to establish two-way databinding on an element. Its value is the name of the attribute. This can be used to set the value of form inputs or to change text nodes. For example,
|
122
|
+
|
123
|
+
```html
|
124
|
+
<input type="text" name="content" template-attr="content">
|
125
|
+
<!-- or alternatively -->
|
126
|
+
<span template-attr="content">
|
127
|
+
```
|
128
|
+
|
129
|
+
- **all form submits are intercepted** by event listeners by default. To override this, add the "skip-sockets" attribute to the form element. They submit AJAX requests using the url in the form's `action` attribute and the method in the form's `method` attribute (i.e. `action="/todos" method="POST"`). This works for `GET` and `POST` only, but `PUT` and `DELETE` can be used by adding a hidden input method i.e. `input type="hidden" name="_method" value="PUT"`. This is the default Rails behavior anyway.
|
130
|
+
|
131
|
+
- To submit an id with a form, bind a hidden attribute i.e. `<input type="hidden" name="id" template-attr='id'>`
|
132
|
+
|
133
|
+
- Outside of `[template]`s, binding tags are a bit more verbose. `<span binding-tag='todos,1,content'></span>` where the three comma-separated arguments are `<model_class>`, `<id>`, and `<attribute>`. `template-attr` tags are automatically converted to `binding-tag` once new records are added to the page.
|
134
|
+
|
135
|
+
---
|
136
|
+
|
137
|
+
### **Other notes**
|
138
|
+
|
139
|
+
#### **Changing a classes' published class name
|
140
|
+
|
141
|
+
- Say I created a `LocationCategorization` scaffold but
|
142
|
+
realized that I would rather publish the data using
|
143
|
+
a `record_class` value of `category` instead of `location_categorization`.
|
144
|
+
I don't want to undo the scaffold, so I add a method to the `LocationCategorization` class:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class LocationCategorization < ActiveRecord::Base
|
148
|
+
def published_class
|
149
|
+
"category"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
This particular method name is used as an optional override
|
155
|
+
for the default published class name (`record.class.to_s.underscore`)
|
156
|
+
|
157
|
+
#### **Loading initial data on the page**
|
158
|
+
|
159
|
+
Without doing this, the page will be empty every time it is refreshed. The page needs to start out with a list of records loaded.
|
160
|
+
|
161
|
+
Create an html element with an `init` attribute set to a model class, i.e. `todo`. This element will be auto-hidden. In the html-serving controller method, make an instance variable for whatever data is going to be included (expects an array, not a single object or query). On the html page, use ERB to set the content of the `[init]` element to a JSON stringified version of your instance variable. For example, `<div init="todo"><%= Oj.dump([User.first]) %></div>`
|
162
|
+
|
163
|
+
#### **How to do links with params**
|
164
|
+
|
165
|
+
i.e. how to do
|
166
|
+
|
167
|
+
```html
|
168
|
+
<a href="/my_link?with=params">My Link </a>
|
169
|
+
```
|
170
|
+
|
171
|
+
The way to do this is by building a form and disguising it as a link. Basically come up with some CSS style so the form looks like a link. I don't really know how to do the CSS, but the form HTML code is below. This has the effect of creating a button on the page with the desired link follow-through when clicked. In this example, the 'link-style' class has to be externally implemented.
|
172
|
+
|
173
|
+
```html
|
174
|
+
<form skip-sockets class="link-style" action="/notepad" method="GET">
|
175
|
+
<input type="hidden" name="name" template-attr='name'>
|
176
|
+
<input type="submit" template-attr='name'>
|
177
|
+
</form>
|
178
|
+
|
179
|
+
```
|
180
|
+
|
181
|
+
|
182
|
+
|
183
|
+
### **Additional Helpers**
|
184
|
+
|
185
|
+
you can make one html element toggle another open / close very easily.
|
186
|
+
|
187
|
+
Just make them 'siblings (share the same parent element) and give the trigger a `toggles` attribute with a value set to the CSS selector of the target. The target will be initially closed.
|
188
|
+
|
189
|
+
---
|
190
|
+
|
191
|
+
### **Use of OJ gem for JSON**
|
192
|
+
|
193
|
+
- I use the OJ gem here and `Oj.dump` because of a recursion bug in `to_json`. I'm still not sure what the cause is, perhaps a naming conflict somewhere. Also, `Oj.dump` only works with single elements / arrays, not active record queries i.e. `Oj.dump Todo.all.limit(5)` wouldnt work.
|
194
|
+
- However Oj seems pretty legit.
|
195
|
+
- The way to Json-stringify records for a websocket-publish action is:
|
196
|
+
```ruby
|
197
|
+
Oj.dump(
|
198
|
+
records.map do |record|
|
199
|
+
record.attributes.merge(
|
200
|
+
'record_class' => record.class.to_s.underscore,
|
201
|
+
)
|
202
|
+
end
|
203
|
+
)
|
204
|
+
```
|
205
|
+
- This is done automatically when using `websocket_response`,
|
206
|
+
but needs to be added otherwise (like when using server seeded data
|
207
|
+
to set a page's initial state)
|
data/Rakefile
ADDED
@@ -0,0 +1,267 @@
|
|
1
|
+
$(function(){
|
2
|
+
|
3
|
+
curry = function (fn) {
|
4
|
+
var slice = [].slice,
|
5
|
+
args = slice.call(arguments, 1);
|
6
|
+
return function () {
|
7
|
+
return fn.apply(this, args.concat(slice.call(arguments)));
|
8
|
+
};
|
9
|
+
};
|
10
|
+
|
11
|
+
|
12
|
+
Style = {
|
13
|
+
createStylesheet: function(){
|
14
|
+
var style = document.createElement("style")
|
15
|
+
// Add a media (and/or media query) here if you'd like!
|
16
|
+
// style.setAttribute("media", "screen")
|
17
|
+
// style.setAttribute("media", "only screen and (max-width : 1024px)")
|
18
|
+
style.appendChild(document.createTextNode(""));
|
19
|
+
document.head.appendChild(style)
|
20
|
+
return style.sheet
|
21
|
+
},
|
22
|
+
stylesheet: function(){
|
23
|
+
if(this._stylesheet){return this._stylesheet}
|
24
|
+
else {
|
25
|
+
this._stylesheet = this.createStylesheet();
|
26
|
+
return this._stylesheet
|
27
|
+
}
|
28
|
+
},
|
29
|
+
addCSSRule: function(selector, rules, index) {
|
30
|
+
var sheet = this.stylesheet()
|
31
|
+
if("insertRule" in sheet) {
|
32
|
+
sheet.insertRule(selector + "{" + rules + "}", index);
|
33
|
+
}
|
34
|
+
else if("addRule" in sheet) {
|
35
|
+
sheet.addRule(selector, rules, index);
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
// uses 'binding-tag' attr
|
41
|
+
// format: binding-tag='class_name,id,attribute'
|
42
|
+
getBindingTags = function (css_selector){
|
43
|
+
var bindings = []
|
44
|
+
bindingTags = $(css_selector).find("[binding-tag]")
|
45
|
+
$.each(bindingTags, function(index, tag){
|
46
|
+
var $tagElement = $(tag)
|
47
|
+
var tagName = $tagElement.attr("binding-tag")
|
48
|
+
var components = tagName.split(",")
|
49
|
+
var model_name = components[0]
|
50
|
+
var id = components[1]
|
51
|
+
var attribute = components[2]
|
52
|
+
bindings.push({
|
53
|
+
dom_elements: $("*[binding-tag='"+tagName+"']"),
|
54
|
+
model_name: model_name,
|
55
|
+
id: id,
|
56
|
+
attribute: attribute,
|
57
|
+
})
|
58
|
+
})
|
59
|
+
return bindings
|
60
|
+
}
|
61
|
+
|
62
|
+
initBindings = function(bindings) {
|
63
|
+
$.each(bindings, function(index, binding){
|
64
|
+
var $dom_elements = binding['dom_elements']
|
65
|
+
var model_name = binding['model_name']
|
66
|
+
var id = binding['id']
|
67
|
+
var attribute = binding['attribute']
|
68
|
+
var channel = channels[model_name]
|
69
|
+
channel.bind(
|
70
|
+
(model_name+'-'+id+'-'+attribute+'-update'),
|
71
|
+
curry(updateElement, $dom_elements)
|
72
|
+
)
|
73
|
+
})
|
74
|
+
}
|
75
|
+
|
76
|
+
updateElement = function($dom_elements, new_value) {
|
77
|
+
$dom_elements.val(new_value)
|
78
|
+
$dom_elements.attr("value", new_value)
|
79
|
+
$dom_elements.text(new_value)
|
80
|
+
}
|
81
|
+
|
82
|
+
addElement = function($container, $completedTemplate){
|
83
|
+
$container.append(
|
84
|
+
$("<div>").append($completedTemplate.clone()).html()
|
85
|
+
)
|
86
|
+
}
|
87
|
+
|
88
|
+
deserialize = function(record){
|
89
|
+
return JSON.parse(record)
|
90
|
+
}
|
91
|
+
|
92
|
+
processNewRecords = function(serializedRecords){
|
93
|
+
serializedRecords = $.makeArray(serializedRecords)
|
94
|
+
serializedRecords.forEach(function(record){
|
95
|
+
record = deserialize(record)
|
96
|
+
model_name = record['record_class']
|
97
|
+
$container = $("."+model_name+'-index')
|
98
|
+
$template = $container.find("[template]")
|
99
|
+
$templateClone = cloneTemplate($template, record)
|
100
|
+
$templateClone.removeAttr("template")
|
101
|
+
$templateClone.attr(
|
102
|
+
"record-id",
|
103
|
+
(model_name+","+record['id'])
|
104
|
+
)
|
105
|
+
addElement($container, $templateClone)
|
106
|
+
initBindings(getBindingTags($templateClone))
|
107
|
+
initToggleInitialState($templateClone)
|
108
|
+
processUpdateRecords(JSON.stringify(record))
|
109
|
+
})
|
110
|
+
}
|
111
|
+
|
112
|
+
cloneTemplate = function($template, record){
|
113
|
+
$template = $template.clone()
|
114
|
+
templateAttrs = $template.find("[template-attr]")
|
115
|
+
var id = record['id']
|
116
|
+
$.each(templateAttrs, function(index, elem){
|
117
|
+
var $elem = $(elem)
|
118
|
+
var model_name = record['record_class']
|
119
|
+
var attr = $elem.attr('template-attr')
|
120
|
+
$elem.text(record[attr])
|
121
|
+
$elem.val(record[attr])
|
122
|
+
$elem.removeAttr('template-attr')
|
123
|
+
$elem.attr("binding-tag", model_name+','+id+','+attr)
|
124
|
+
})
|
125
|
+
return $template
|
126
|
+
}
|
127
|
+
|
128
|
+
findMatchingNodes = function(record_class, id){
|
129
|
+
return $("*[record-id='"+record_class+","+id)
|
130
|
+
}
|
131
|
+
|
132
|
+
processUpdateRecords = function(serializedRecords){
|
133
|
+
serializedRecords = $.makeArray(serializedRecords)
|
134
|
+
serializedRecords.forEach(function(record){
|
135
|
+
record = deserialize(record)
|
136
|
+
model_name = record['record_class']
|
137
|
+
id = record['id']
|
138
|
+
for (var key in record){
|
139
|
+
var new_val = record[key]
|
140
|
+
console.log(record)
|
141
|
+
var bindingTag = model_name+','+id+','+key
|
142
|
+
channels[model_name].trigger(
|
143
|
+
(model_name+'-'+id+'-'+key+'-update'),
|
144
|
+
new_val
|
145
|
+
)
|
146
|
+
// for some reason this is also necessary when
|
147
|
+
// the binding elements are not in a [template]
|
148
|
+
// although this should be called by the above 'trigger'
|
149
|
+
updateElement(
|
150
|
+
$("[binding-tag='"+bindingTag+"']"),
|
151
|
+
new_val
|
152
|
+
)
|
153
|
+
}
|
154
|
+
})
|
155
|
+
}
|
156
|
+
|
157
|
+
processDestroyRecords = function(serializedRecords){
|
158
|
+
serializedRecords = $.makeArray(serializedRecords)
|
159
|
+
serializedRecords.forEach(function(record){
|
160
|
+
record = deserialize(record)
|
161
|
+
$matchingNodes = findMatchingNodes(
|
162
|
+
record['record_class'],
|
163
|
+
record['id']
|
164
|
+
)
|
165
|
+
$matchingNodes.remove()
|
166
|
+
})
|
167
|
+
}
|
168
|
+
|
169
|
+
|
170
|
+
initFormBindings = function(selector){
|
171
|
+
if (!selector){
|
172
|
+
selector = "*"
|
173
|
+
}
|
174
|
+
$(document).on("submit", (selector+" form"), function(e){
|
175
|
+
var $form = $(e.currentTarget)
|
176
|
+
if ($form[0].hasAttribute("skip-sockets")) {
|
177
|
+
return true
|
178
|
+
}
|
179
|
+
e.preventDefault();
|
180
|
+
var serializedAttrs = $form.serialize()
|
181
|
+
var action = $form.attr("action")
|
182
|
+
var method = $form.attr("method")
|
183
|
+
var $methodOverride = $form.find("[name=_method]")
|
184
|
+
if ($methodOverride.length > 0){
|
185
|
+
method = $methodOverride.val()
|
186
|
+
}
|
187
|
+
$.ajax({
|
188
|
+
url: action,
|
189
|
+
method: method,
|
190
|
+
data: serializedAttrs
|
191
|
+
})
|
192
|
+
return false
|
193
|
+
})
|
194
|
+
}
|
195
|
+
|
196
|
+
|
197
|
+
initToggleInitialState = function(selector){
|
198
|
+
var $togglers = $(selector).find("[toggles]")
|
199
|
+
$.each($togglers, function(index, e){
|
200
|
+
var $toggler = $(e)
|
201
|
+
var target = $toggler.attr("toggles")
|
202
|
+
var $target = $toggler.siblings(target)
|
203
|
+
$target.hide()
|
204
|
+
$toggler.attr("hiding", "")
|
205
|
+
})
|
206
|
+
}
|
207
|
+
|
208
|
+
initTogglerListeners = function(selector){
|
209
|
+
$(document).on("click", (selector+" [toggles]"), function(e){
|
210
|
+
var $toggler = $(e.currentTarget)
|
211
|
+
var target = $toggler.attr("toggles")
|
212
|
+
var $target = $toggler.siblings(target)
|
213
|
+
if ($toggler[0].hasAttribute('hiding')){
|
214
|
+
$toggler.removeAttr('hiding')
|
215
|
+
$target.show()
|
216
|
+
} else {
|
217
|
+
$toggler.attr("hiding", "")
|
218
|
+
$target.hide()
|
219
|
+
}
|
220
|
+
})
|
221
|
+
}
|
222
|
+
|
223
|
+
initServerSeeds = function(){
|
224
|
+
$.each($("[init]"), function(index, init){
|
225
|
+
var $init = $(init)
|
226
|
+
var model_name = $init.attr("init")
|
227
|
+
var serialized_records = $init.text()
|
228
|
+
var records = deserialize(serialized_records)
|
229
|
+
records.forEach(function(record){
|
230
|
+
processNewRecords(JSON.stringify(record))
|
231
|
+
})
|
232
|
+
$init.hide()
|
233
|
+
})
|
234
|
+
}
|
235
|
+
|
236
|
+
SocketHelpers = {
|
237
|
+
addedHooks: [],
|
238
|
+
initialize: function(classes, websocketBaseurl){
|
239
|
+
// hide templates
|
240
|
+
Style.addCSSRule("[template]", "display: none")
|
241
|
+
// server hooks
|
242
|
+
dispatcher = new WebSocketRails(websocketBaseurl)
|
243
|
+
channels = {}
|
244
|
+
classes.forEach(function(class_name){
|
245
|
+
var name = class_name
|
246
|
+
channels[name] = dispatcher.subscribe(name)
|
247
|
+
var channel = channels[name]
|
248
|
+
channel.bind('create', processNewRecords)
|
249
|
+
channel.bind('update', processUpdateRecords)
|
250
|
+
channel.bind('destroy', processDestroyRecords)
|
251
|
+
// SocketHelpers.addedHooks.forEach(function(hook){
|
252
|
+
// var channelName = hook['channelName'],
|
253
|
+
// callback = hook['callback']
|
254
|
+
// channels
|
255
|
+
// })
|
256
|
+
})
|
257
|
+
|
258
|
+
// client hooks
|
259
|
+
initBindings(getBindingTags("*"))
|
260
|
+
initFormBindings("*")
|
261
|
+
initToggleInitialState("*")
|
262
|
+
initTogglerListeners("*")
|
263
|
+
initServerSeeds()
|
264
|
+
return dispatcher
|
265
|
+
}
|
266
|
+
}
|
267
|
+
})
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "socket_helpers"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "socket_helpers/version"
|
2
|
+
require 'rails'
|
3
|
+
|
4
|
+
module SocketHelpers
|
5
|
+
class MyRailtie < Rails::Railtie
|
6
|
+
end
|
7
|
+
module ControllerHelpers
|
8
|
+
require 'oj'
|
9
|
+
def public_attrs(record)
|
10
|
+
attrs = record.attributes.merge('record_class' => record.try(:published_class) || record.class.to_s.underscore)
|
11
|
+
return Oj.dump(attrs)
|
12
|
+
end
|
13
|
+
def websocket_response(records, action)
|
14
|
+
records = [records] unless records.is_a?(Array)
|
15
|
+
records.each do |record|
|
16
|
+
class_name = record.try(:published_class) || record.class.to_s.underscore
|
17
|
+
puts "triggered #{class_name} #{action}"
|
18
|
+
WebsocketRails[class_name].trigger(action, public_attrs(record))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
class Engine < Rails::Engine
|
23
|
+
initializer "my_engine.remove_rack_lock" do |app|
|
24
|
+
app.middleware.delete Rack::Lock
|
25
|
+
end
|
26
|
+
initializer :assets do |config|
|
27
|
+
Rails.application.config.assets.precompile += %w{ socket_helpers.js }
|
28
|
+
Rails.application.config.assets.paths << root.join("app", "assets", "javascripts")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
# require 'socket_helpers/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "socket_helpers"
|
8
|
+
spec.version = "0.0.9"
|
9
|
+
spec.authors = ["maxpleaner"]
|
10
|
+
spec.email = ["maxpleaner@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{websocket helpers for rails}
|
13
|
+
spec.description = %q{websocket helpers for rails}
|
14
|
+
spec.homepage = "http://github.com/maxpleaner/socket_helpers"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(socket_helpers-0.0.9.gem|socket_helpers.gemspec|test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib", "app/controllers"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_runtime_dependency "rails"
|
25
|
+
spec.add_runtime_dependency 'jquery-rails'
|
26
|
+
spec.add_runtime_dependency "jquery-turbolinks"
|
27
|
+
spec.add_runtime_dependency "websocket-rails"
|
28
|
+
spec.add_runtime_dependency "faye-websocket", '0.10.0'
|
29
|
+
spec.add_runtime_dependency "nokogiri"
|
30
|
+
spec.add_runtime_dependency 'oj'
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: socket_helpers
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.9
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- maxpleaner
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.11'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.11'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: jquery-rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: jquery-turbolinks
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: websocket-rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: faye-websocket
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.10.0
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.10.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: nokogiri
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: oj
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: websocket helpers for rails
|
140
|
+
email:
|
141
|
+
- maxpleaner@gmail.com
|
142
|
+
executables: []
|
143
|
+
extensions: []
|
144
|
+
extra_rdoc_files: []
|
145
|
+
files:
|
146
|
+
- ".gitignore"
|
147
|
+
- CODE_OF_CONDUCT.md
|
148
|
+
- Gemfile
|
149
|
+
- LICENSE.txt
|
150
|
+
- README.md
|
151
|
+
- Rakefile
|
152
|
+
- app/assets/javascripts/socket_helpers.js
|
153
|
+
- bin/console
|
154
|
+
- bin/setup
|
155
|
+
- lib/socket_helpers.rb
|
156
|
+
- lib/socket_helpers/version.rb
|
157
|
+
- socket_helpers.gemspec
|
158
|
+
homepage: http://github.com/maxpleaner/socket_helpers
|
159
|
+
licenses:
|
160
|
+
- MIT
|
161
|
+
metadata: {}
|
162
|
+
post_install_message:
|
163
|
+
rdoc_options: []
|
164
|
+
require_paths:
|
165
|
+
- lib
|
166
|
+
- app/controllers
|
167
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
168
|
+
requirements:
|
169
|
+
- - ">="
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '0'
|
172
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
173
|
+
requirements:
|
174
|
+
- - ">="
|
175
|
+
- !ruby/object:Gem::Version
|
176
|
+
version: '0'
|
177
|
+
requirements: []
|
178
|
+
rubyforge_project:
|
179
|
+
rubygems_version: 2.5.1
|
180
|
+
signing_key:
|
181
|
+
specification_version: 4
|
182
|
+
summary: websocket helpers for rails
|
183
|
+
test_files: []
|
184
|
+
has_rdoc:
|