polished-knockout 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/CODE_OF_CONDUCT.md +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +42 -0
- data/Rakefile +1 -0
- data/config.ru +14 -0
- data/lib/polished/knockout/helpers/knockout_helpers.rb +14 -0
- data/lib/polished/knockout/railtie.rb +8 -0
- data/lib/polished/knockout/version.rb +5 -0
- data/lib/polished/knockout/view_model.rb +346 -0
- data/lib/polished/knockout.rb +10 -0
- data/lib/polished-knockout.rb +1 -0
- data/polished-knockout.gemspec +26 -0
- data/spec/fixtures/users.json +34 -0
- data/spec/knockout/index.html.erb +11 -0
- data/spec/server_roundtrip_spec.rb +80 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/view_model_spec.rb +119 -0
- metadata +140 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c60a918d9afb8c42b4b3727eaf288a89b5d834a8
|
4
|
+
data.tar.gz: e44e07b46ab356a82f54ced938557afa1ac6c9c8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d8ba6d4b830ded0cd7a9b8cc49da49abba3cd51f2a1410d58bde58b3b5f8b4daf39d5929891585d6b07d077c664a779c9e74fa289a829b1dc7afca530d639ef2
|
7
|
+
data.tar.gz: d8369009288c302bba97b9769e23910f63698873cc1b5f7a125663c49656794b9d0b9a7fcb09a925daf7b37ef679390ec8c4a5d28b1831405c53f45ab2ab4459
|
data/.gitignore
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of overtly sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
This code of conduct applies both within project spaces and in public spaces where an individual explicitly associates their presence with the project; non-project related material on accounts explicitly marked as personal should not be considered to be so associated.
|
12
|
+
|
13
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
14
|
+
|
15
|
+
This Code of Conduct is adapted from the [Opal Code of Conduct](https://github.com/opal/opal/blob/master/CODE_OF_CONDUCT.md), which in turn is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Jared White
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Polished: Knockout
|
2
|
+
|
3
|
+
An [Opal (Ruby-to-JS)](http://opalrb.org) library for creating view models that use Knockout.js for dynamic HTML updates and event handling.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'polished-knockout'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install polished-knockout
|
18
|
+
|
19
|
+
## Getting Started
|
20
|
+
|
21
|
+
Read the [Getting Started](http://polished-rb.github.io/knockout-rb/getting-started/) tutorial to see how easy it is to build view models and load in JSON data. Sneak peak:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class UserView < Knockout::ViewModel
|
25
|
+
bind_accessor :first_name, :last_name, :age, :user_types
|
26
|
+
bind_collection :favorite_foods, class_name: 'FavoriteFoodView'
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
For more in-depth documentation, visit the [Documentation](http://polished-rb.github.io/knockout-rb/docs/) page.
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
1. Fork it ( http://github.com/polished-rb/knockout/fork )
|
35
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
36
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
37
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
38
|
+
5. Create new Pull Request
|
39
|
+
|
40
|
+
## Testing
|
41
|
+
|
42
|
+
Simply run `rackup` at your command line when you're in the project folder. It will load a webserver at port 9292. Then just go to your browser and access `http://localhost:9292`. You should get the full rspec suite runner output. (And hopefully, everything's green!)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/config.ru
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler.require
|
3
|
+
|
4
|
+
require 'opal-rspec'
|
5
|
+
Opal.append_path File.expand_path('../spec', __FILE__)
|
6
|
+
|
7
|
+
require 'opal/jquery'
|
8
|
+
|
9
|
+
run Opal::Server.new { |s|
|
10
|
+
s.main = 'opal/rspec/sprockets_runner'
|
11
|
+
s.append_path 'spec'
|
12
|
+
s.debug = false
|
13
|
+
s.index_path = 'spec/knockout/index.html.erb'
|
14
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module PolishedKnockout
|
2
|
+
module KnockoutHelpers
|
3
|
+
def knockout(partial=nil, subview:nil, bind_as:nil)
|
4
|
+
if partial
|
5
|
+
render(partial: 'knockout/' + partial.to_s)
|
6
|
+
elsif subview
|
7
|
+
subview_with = bind_as || subview.split('/').last
|
8
|
+
content_tag(:div, 'data-bind' => "with: #{subview_with}") do
|
9
|
+
render(partial: 'knockout/' + subview.to_s)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'polished/knockout/helpers/knockout_helpers'
|
2
|
+
module PolishedKnockout
|
3
|
+
class Railtie < Rails::Railtie
|
4
|
+
initializer "polished_knockout.knockout_helpers" do
|
5
|
+
ActiveSupport.on_load( :action_view ){ include PolishedKnockout::KnockoutHelpers }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
# Knockout::ViewModel provides a simple Opal-based Ruby wrapper around
|
2
|
+
# Knockout.js, a popular front-end data-binding and events library.
|
3
|
+
#
|
4
|
+
# Run one or more of these class methods in a Knockout::ViewModel
|
5
|
+
# subclass, aka
|
6
|
+
#
|
7
|
+
# bind_var :somevar, :anothervar
|
8
|
+
# bind_event :clickme
|
9
|
+
#
|
10
|
+
# and then wrappers will be created for the JS model that is passed to
|
11
|
+
# Knockout
|
12
|
+
#
|
13
|
+
# You can either do attr_accessor :somevar, :anothervar or write custom
|
14
|
+
# methods in your class, or you can use the shortcut bind_accessor to set
|
15
|
+
# up bind_var and attr_accessor at once.
|
16
|
+
#
|
17
|
+
# You'll need to use bind_id on a top-level view to target an element in your
|
18
|
+
# DOM with data-bind-id:
|
19
|
+
#
|
20
|
+
# bind_id :some-id
|
21
|
+
#
|
22
|
+
# Example HTML:
|
23
|
+
# <div data-bind-id="some-id">
|
24
|
+
# <span data-bind="text: somevar"> </span>, <span data-bind="html: anothervar"> </span>
|
25
|
+
# </div>
|
26
|
+
#
|
27
|
+
# Methods in your class are passed a jQuery event object, for example:
|
28
|
+
#
|
29
|
+
# <a href="#" data-bind="click: clickme">Click me!</a>
|
30
|
+
#
|
31
|
+
# def clickme(event)
|
32
|
+
# puts event.page_x
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# TODO: remove dependency on jQuery events, so for example opal-browser events
|
36
|
+
# could also be used
|
37
|
+
#
|
38
|
+
# -----
|
39
|
+
#
|
40
|
+
# Subview docs forthcoming...
|
41
|
+
#
|
42
|
+
|
43
|
+
module Knockout
|
44
|
+
class ViewModel
|
45
|
+
|
46
|
+
def self.bind_setup
|
47
|
+
if @bind_defs.nil?
|
48
|
+
@bind_defs = {
|
49
|
+
:vars => [],
|
50
|
+
:subviews => [],
|
51
|
+
:collections => [],
|
52
|
+
:events => [],
|
53
|
+
:methods => []
|
54
|
+
}
|
55
|
+
attr_accessor :parent_view
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.bind_id(bind_id)
|
60
|
+
bind_setup
|
61
|
+
@bind_defs[:id] = bind_id
|
62
|
+
end
|
63
|
+
def self.bind_var(*arr)
|
64
|
+
bind_setup
|
65
|
+
@bind_defs[:vars] += arr
|
66
|
+
end
|
67
|
+
def self.bind_accessor(*arr)
|
68
|
+
bind_setup
|
69
|
+
@bind_defs[:vars] += arr
|
70
|
+
attr_accessor(*arr)
|
71
|
+
end
|
72
|
+
def self.bind_collection(name, options=nil)
|
73
|
+
bind_setup
|
74
|
+
@bind_defs[:collections] << {
|
75
|
+
varname: name,
|
76
|
+
class_name: options.is_a?(Hash) ? options[:class_name] : nil
|
77
|
+
}
|
78
|
+
attr_accessor(name)
|
79
|
+
end
|
80
|
+
def self.bind_event(*arr)
|
81
|
+
bind_setup
|
82
|
+
@bind_defs[:events] += arr
|
83
|
+
end
|
84
|
+
def self.bind_method(*arr)
|
85
|
+
bind_setup
|
86
|
+
@bind_defs[:methods] += arr
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.bind_defs
|
90
|
+
@bind_defs
|
91
|
+
end
|
92
|
+
|
93
|
+
def initialize
|
94
|
+
begin
|
95
|
+
@bound_model = `{}`
|
96
|
+
self.class.bind_defs()[:vars].each do |var_label|
|
97
|
+
if self.send(var_label).is_a?(KnockoutArray)
|
98
|
+
ko_arr_observable = self.send(var_label).to_n
|
99
|
+
`self.bound_model[var_label] = ko_arr_observable`
|
100
|
+
# At some point, figure out how to make it so
|
101
|
+
# foomodel.array = ['abc', 123]
|
102
|
+
# actually just clears and adds back in stuff via the magic
|
103
|
+
# KnockoutArray stuff
|
104
|
+
#
|
105
|
+
# define_singleton_method("#{var_label}=") do |val|
|
106
|
+
# result = super val
|
107
|
+
# attribute_did_change(attribute)
|
108
|
+
# result
|
109
|
+
# end
|
110
|
+
else
|
111
|
+
`self.bound_model[var_label] = ko.observable(null)`
|
112
|
+
`var skipit = '_skip_observable_' + var_label`
|
113
|
+
`self.bound_model[var_label].subscribe(function(newValue) {`
|
114
|
+
`if (typeof self[skipit] == 'undefined' || self[skipit] != true) {
|
115
|
+
self[skipit] = true`
|
116
|
+
self.send(var_label + "=", `newValue`)
|
117
|
+
`self[skipit] = false`
|
118
|
+
`}`
|
119
|
+
`});`
|
120
|
+
self.add_observer(var_label) do |new_val|
|
121
|
+
if new_val.is_a?(Array) or new_val.is_a?(Hash) or new_val.is_a?(ViewModel)
|
122
|
+
new_val = new_val.to_n # to_n converts an Opal-based object to a "native" JS object
|
123
|
+
end
|
124
|
+
`if (typeof self[skipit] == 'undefined' || self[skipit] != true) { `
|
125
|
+
`self[skipit] = true`
|
126
|
+
`self.bound_model[var_label](new_val)`
|
127
|
+
`}`
|
128
|
+
`self[skipit] = false`
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end if self.class.bind_defs()[:vars]
|
132
|
+
|
133
|
+
self.class.bind_defs()[:collections].each do |collection|
|
134
|
+
var_label = collection[:varname]
|
135
|
+
ko_arr = KnockoutArray.new
|
136
|
+
ko_arr.set_collection_class(collection[:class_name], self)
|
137
|
+
self.send(var_label + '=', ko_arr)
|
138
|
+
ko_arr_observable = self.send(var_label).to_n
|
139
|
+
`self.bound_model[var_label] = ko_arr_observable`
|
140
|
+
end if self.class.bind_defs()[:collections]
|
141
|
+
|
142
|
+
self.class.bind_defs()[:events].each do |method_label|
|
143
|
+
`self.bound_model[method_label] = function(data, event) { return self['$handle_bind_event'](method_label, event) }`
|
144
|
+
end if self.class.bind_defs()[:events]
|
145
|
+
|
146
|
+
self.class.bind_defs()[:methods].each do |method_label|
|
147
|
+
`self.bound_model[method_label] = function(data) { return self['$handle_bind_method'](method_label, data) }`
|
148
|
+
end if self.class.bind_defs()[:methods]
|
149
|
+
|
150
|
+
if self.class.bind_defs()[:id]
|
151
|
+
bind_id = self.class.bind_defs()[:id]
|
152
|
+
bind_el = `$('[data-bind-id=' + bind_id + ']')`
|
153
|
+
`ko.applyBindings(self.bound_model, bind_el.get(0))`
|
154
|
+
`bind_el.addClass('ko-bound')`
|
155
|
+
end
|
156
|
+
rescue Exception => e
|
157
|
+
Element.find('body').add_class('ko-debug')
|
158
|
+
raise e
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.new_via_collection(hash_obj, parent=nil)
|
163
|
+
new_object = self.new
|
164
|
+
new_object.parent_view = parent if parent
|
165
|
+
hash_obj.each do |k,v|
|
166
|
+
if v.is_a?(Array) and new_object.send(k).is_a?(KnockoutArray)
|
167
|
+
ko_arr = new_object.send(k)
|
168
|
+
ko_arr.clear
|
169
|
+
ko_arr.concat(v)
|
170
|
+
else
|
171
|
+
new_object.send(k + "=", v)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
new_object
|
175
|
+
end
|
176
|
+
|
177
|
+
def self.new_with_parent(parent, *arr)
|
178
|
+
new_object = self.new(*arr)
|
179
|
+
new_object.parent_view = parent
|
180
|
+
new_object
|
181
|
+
end
|
182
|
+
|
183
|
+
def bound_model
|
184
|
+
@bound_model
|
185
|
+
end
|
186
|
+
|
187
|
+
def handle_bind_event(method_label, event)
|
188
|
+
wrapped_event = Event.new(event)
|
189
|
+
self.send(method_label, wrapped_event)
|
190
|
+
end
|
191
|
+
|
192
|
+
def handle_bind_method(method_label, data)
|
193
|
+
self.send(method_label, data)
|
194
|
+
end
|
195
|
+
|
196
|
+
### Override the standard to_n feature by passing along the Knockout model obj instead
|
197
|
+
def to_n
|
198
|
+
bound_model
|
199
|
+
end
|
200
|
+
|
201
|
+
def to_json
|
202
|
+
`ko.toJSON(#{@bound_model})`
|
203
|
+
end
|
204
|
+
|
205
|
+
def serialize_js_data
|
206
|
+
`ko.toJS(#{@bound_model})`
|
207
|
+
end
|
208
|
+
|
209
|
+
### The following Ruby observer code was borrowed from Vienna::Observable
|
210
|
+
### https://github.com/opal/vienna
|
211
|
+
def add_observer(attribute, &handler)
|
212
|
+
unless observers = @attr_observers
|
213
|
+
observers = @attr_observers = {}
|
214
|
+
end
|
215
|
+
|
216
|
+
unless handlers = observers[attribute]
|
217
|
+
handlers = observers[attribute] = []
|
218
|
+
replace_writer_for(attribute)
|
219
|
+
end
|
220
|
+
|
221
|
+
handlers << handler
|
222
|
+
end
|
223
|
+
|
224
|
+
def remove_observer(attribute, handler)
|
225
|
+
return unless @attr_observers
|
226
|
+
|
227
|
+
if handlers = @attr_observers[attribute]
|
228
|
+
handlers.delete handler
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Triggers observers for the given attribute. You may call this directly if
|
233
|
+
# needed, but it is generally called automatically for you inside a
|
234
|
+
# replaced setter method.
|
235
|
+
def attribute_did_change(attribute)
|
236
|
+
return unless @attr_observers
|
237
|
+
|
238
|
+
if handlers = @attr_observers[attribute]
|
239
|
+
new_val = __send__(attribute) if respond_to?(attribute)
|
240
|
+
handlers.each { |h| h.call new_val }
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# private?
|
245
|
+
def replace_writer_for(attribute)
|
246
|
+
if respond_to? "#{attribute}="
|
247
|
+
define_singleton_method("#{attribute}=") do |val|
|
248
|
+
result = super val
|
249
|
+
attribute_did_change(attribute)
|
250
|
+
result
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# This class is used internally and you shouldn't need to initialize it
|
258
|
+
# yourself or worry too much whether an array is a standard array or a
|
259
|
+
# KnockoutArray
|
260
|
+
#
|
261
|
+
class KnockoutArray < Array
|
262
|
+
# NOTE: this method has to be run right after a KnockoutArray is first
|
263
|
+
# initialized
|
264
|
+
def to_n
|
265
|
+
array_value = super
|
266
|
+
|
267
|
+
@ko_observable = `ko.observableArray(array_value)`
|
268
|
+
|
269
|
+
@ko_observable
|
270
|
+
end
|
271
|
+
|
272
|
+
def to_json
|
273
|
+
`ko.toJSON(#{@ko_observable})`
|
274
|
+
end
|
275
|
+
|
276
|
+
def serialize_js_data
|
277
|
+
`ko.toJS(#{@ko_observable})`
|
278
|
+
end
|
279
|
+
|
280
|
+
def set_collection_class(class_name, collection_parent)
|
281
|
+
@collection_class_name = class_name if class_name
|
282
|
+
@collection_parent = collection_parent
|
283
|
+
end
|
284
|
+
|
285
|
+
def collection_check(obj)
|
286
|
+
if @collection_class_name and obj.is_a?(Hash)
|
287
|
+
Kernel.const_get(@collection_class_name).new_via_collection(obj, @collection_parent)
|
288
|
+
else
|
289
|
+
obj
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def concat(other)
|
294
|
+
if Array === other
|
295
|
+
other = other.to_a
|
296
|
+
else
|
297
|
+
other = Opal.coerce_to(other, Array, :to_ary).to_a
|
298
|
+
end
|
299
|
+
|
300
|
+
other.each do |item|
|
301
|
+
self.send('<<', item)
|
302
|
+
end
|
303
|
+
|
304
|
+
self
|
305
|
+
end
|
306
|
+
|
307
|
+
def <<(obj)
|
308
|
+
obj = collection_check(obj)
|
309
|
+
|
310
|
+
ret = super(obj)
|
311
|
+
|
312
|
+
`self.ko_observable.push(obj.$to_n())`
|
313
|
+
|
314
|
+
ret
|
315
|
+
end
|
316
|
+
|
317
|
+
def delete_at(index)
|
318
|
+
ret = super(index)
|
319
|
+
|
320
|
+
unless ret == nil
|
321
|
+
`self.ko_observable.splice(index, 1)`
|
322
|
+
end
|
323
|
+
|
324
|
+
ret
|
325
|
+
end
|
326
|
+
|
327
|
+
def delete(obj)
|
328
|
+
obj_index = index(obj)
|
329
|
+
ret = nil
|
330
|
+
|
331
|
+
unless obj_index == nil
|
332
|
+
ret = super(obj)
|
333
|
+
`self.ko_observable.splice(obj_index, 1)`
|
334
|
+
end
|
335
|
+
|
336
|
+
ret
|
337
|
+
end
|
338
|
+
|
339
|
+
def clear()
|
340
|
+
ret = super
|
341
|
+
|
342
|
+
`self.ko_observable.removeAll()`
|
343
|
+
|
344
|
+
ret
|
345
|
+
end
|
346
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
if RUBY_ENGINE == 'opal'
|
2
|
+
require 'polished/knockout/view_model'
|
3
|
+
else
|
4
|
+
require 'opal'
|
5
|
+
require 'polished/knockout/version'
|
6
|
+
|
7
|
+
Opal.append_path File.expand_path('../..', __FILE__).untaint
|
8
|
+
|
9
|
+
require 'polished/knockout/railtie' if defined?(Rails::Railtie)
|
10
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'polished/knockout'
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'polished/knockout/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "polished-knockout"
|
8
|
+
spec.version = Polished::Knockout::VERSION
|
9
|
+
spec.authors = ["Jared White"]
|
10
|
+
spec.email = ["jared@jaredwhite.com"]
|
11
|
+
spec.description = %q{An Opal library for creating view models that use Knockout.js for dynamic HTML updates and event handling}
|
12
|
+
spec.summary = spec.description
|
13
|
+
spec.homepage = "https://github.com/polished-rb/knockout-rb"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency 'opal', '~> 0.8'
|
22
|
+
spec.add_dependency 'opal-jquery', '~> 0.4'
|
23
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency 'opal-rspec', '~> 0.4'
|
26
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"first_name": "Jared",
|
4
|
+
"last_name": "White",
|
5
|
+
"age": 33,
|
6
|
+
"user_types": ["Male", "Human"],
|
7
|
+
"favorite_foods": [
|
8
|
+
{
|
9
|
+
"name": "Burrito",
|
10
|
+
"origin": "Mexico"
|
11
|
+
},
|
12
|
+
{
|
13
|
+
"name": "Pizza",
|
14
|
+
"origin": "Italy"
|
15
|
+
}
|
16
|
+
]
|
17
|
+
},
|
18
|
+
{
|
19
|
+
"first_name": "Jasmine",
|
20
|
+
"last_name": "Kitty",
|
21
|
+
"age": 6,
|
22
|
+
"user_types": ["Female", "Cat"],
|
23
|
+
"favorite_foods": [
|
24
|
+
{
|
25
|
+
"name": "Fish",
|
26
|
+
"origin": "Ocean"
|
27
|
+
},
|
28
|
+
{
|
29
|
+
"name": "Kibble",
|
30
|
+
"origin": "Pet Store"
|
31
|
+
}
|
32
|
+
]
|
33
|
+
}
|
34
|
+
]
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Opal Knockout - RSpec Runner</title>
|
5
|
+
<script src="https://code.jquery.com/jquery-1.8.3.min.js"></script>
|
6
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js"></script>
|
7
|
+
<%= javascript_include_tag @server.main %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
</body>
|
11
|
+
</html>
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
#### ViewModel Classes ####
|
4
|
+
class UsersView < Knockout::ViewModel
|
5
|
+
bind_id "test-users"
|
6
|
+
bind_collection :users, class_name: 'UserView'
|
7
|
+
end
|
8
|
+
|
9
|
+
class UserView < Knockout::ViewModel
|
10
|
+
bind_accessor :first_name, :last_name, :age, :user_types
|
11
|
+
bind_collection :favorite_foods, class_name: 'FavoriteFoodView'
|
12
|
+
end
|
13
|
+
|
14
|
+
class FavoriteFoodView < Knockout::ViewModel
|
15
|
+
bind_accessor :name
|
16
|
+
bind_var :origin
|
17
|
+
|
18
|
+
def origin=(val)
|
19
|
+
@origin = val
|
20
|
+
end
|
21
|
+
def origin
|
22
|
+
@origin ? @origin.upcase : ""
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#### Test Routines ####
|
27
|
+
describe 'bindings using server data' do
|
28
|
+
it 'should have HTTP get support' do
|
29
|
+
HTTP.methods.include?(:get)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should save an object graph' do
|
33
|
+
uv = UserView.new_via_collection(first_name: 'Dick', last_name: 'Tracy', age: 25, user_types: ["Male", "Human"])
|
34
|
+
|
35
|
+
# Get the JSON string from the object
|
36
|
+
test_json = uv.to_json
|
37
|
+
|
38
|
+
expect(test_json).to include('{"first_name":"Dick"')
|
39
|
+
expect(test_json).to include('"user_types":["Male","Human"]')
|
40
|
+
|
41
|
+
# JSON postback to server would go here :)
|
42
|
+
|
43
|
+
|
44
|
+
# also make sure real JS data objects work too
|
45
|
+
test_js_graph = uv.serialize_js_data
|
46
|
+
|
47
|
+
expect(`test_js_graph.last_name`).to eq('Tracy')
|
48
|
+
end
|
49
|
+
|
50
|
+
async 'should bind a view with view collection from JSON' do
|
51
|
+
collection_html = '<div id="bind-userstest" data-bind-id="test-users" style="display:none"><ul data-bind="foreach: users"><li><span data-bind="text: first_name">_</span> loves <span class="favfoods" data-bind="foreach: favorite_foods"><span data-bind="text: name">_</span> from <span data-bind="text: origin">_</span></span></li></ul></div>'
|
52
|
+
Element.find('body') << collection_html
|
53
|
+
|
54
|
+
users_view = UsersView.new
|
55
|
+
|
56
|
+
req_url = "/spec/fixtures/users.json"
|
57
|
+
HTTP.get(req_url).then do |response|
|
58
|
+
run_async do
|
59
|
+
users_view.users.concat(response.json)
|
60
|
+
expect(Element.find('#bind-userstest > ul > li:eq(0) > span:eq(0)').text).to eq('Jared')
|
61
|
+
expect(Element.find('#bind-userstest > ul > li:eq(1) > span:eq(0)').text).to eq('Jasmine')
|
62
|
+
|
63
|
+
expect(users_view.users[0].age).to eq(33)
|
64
|
+
|
65
|
+
expect(users_view.users[1].user_types[1]).to eq('Cat')
|
66
|
+
|
67
|
+
expect(users_view.users[0].favorite_foods[0].origin).to eq('MEXICO')
|
68
|
+
|
69
|
+
expect(Element.find('#bind-userstest > ul > li:eq(0) span.favfoods > span:eq(2)').text).to eq('Pizza')
|
70
|
+
|
71
|
+
users_view.users[0].favorite_foods[1].name = "Lasagna"
|
72
|
+
|
73
|
+
expect(Element.find('#bind-userstest > ul > li:eq(0) span.favfoods > span:eq(2)').text).to eq('Lasagna')
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
#### ViewModel Classes ####
|
4
|
+
class TestBindings < Knockout::ViewModel
|
5
|
+
attr_accessor :text_var
|
6
|
+
|
7
|
+
bind_id "test-bindings"
|
8
|
+
bind_var :text_var
|
9
|
+
bind_accessor :html_var
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
super
|
13
|
+
self.text_var = "Var 123"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class TestSubviewBindings < Knockout::ViewModel
|
18
|
+
bind_id "test-subview-bindings"
|
19
|
+
bind_accessor :somesubview, :somesubview2
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
super
|
23
|
+
|
24
|
+
self.somesubview = TestSubView.new_with_parent(self, 100)
|
25
|
+
self.somesubview2 = TestSubView.new_with_parent(self, 200)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class TestSubView < Knockout::ViewModel
|
30
|
+
bind_accessor :somevar
|
31
|
+
|
32
|
+
def initialize(additional_number)
|
33
|
+
super
|
34
|
+
|
35
|
+
self.somevar = 12345 + additional_number
|
36
|
+
end
|
37
|
+
|
38
|
+
# multiply the incoming value by 100 and make sure that's reflected in the HTML output
|
39
|
+
def somevar=(val)
|
40
|
+
@somevar = val * 100
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class TestCollection < Knockout::ViewModel
|
45
|
+
bind_id "test-collection"
|
46
|
+
bind_collection :items, class_name: 'TestCollectionItem'
|
47
|
+
|
48
|
+
def initialize(data)
|
49
|
+
super
|
50
|
+
self.items.concat(data)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class TestCollectionItem < Knockout::ViewModel
|
55
|
+
bind_accessor :title
|
56
|
+
end
|
57
|
+
|
58
|
+
#### Test Routines ####
|
59
|
+
describe 'knockout bindings' do
|
60
|
+
before(:all) do
|
61
|
+
@view_html = '<div id="bindtest1" data-bind-id="test-bindings" style="display:none"><span class="text-var" data-bind="text: text_var">_</span><span class="html-var" data-bind="html: html_var"></span></div>'
|
62
|
+
|
63
|
+
Element.find('body') << @view_html
|
64
|
+
@view_model = TestBindings.new
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should bind a string var' do
|
68
|
+
var_element = Element.find('#bindtest1 > span.text-var')
|
69
|
+
expect(var_element.text).to eq('Var 123')
|
70
|
+
@view_model.text_var = "Var 456"
|
71
|
+
expect(var_element.text).to eq('Var 456')
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'should bind an HTML var' do
|
75
|
+
var_element = Element.find('#bindtest1 > span.html-var')
|
76
|
+
@view_model.html_var = "<strong><em>This is very strong!</em></strong>"
|
77
|
+
expect(var_element.find('strong > em').text).to eq('This is very strong!')
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should bind a subview and support data manipulation on setters' do
|
81
|
+
subview_html = '<div id="bindtest2" data-bind-id="test-subview-bindings" style="display:none"><div data-bind="with: somesubview"><span class="text-var" data-bind="text: somevar">_</span></div><div data-bind="with: somesubview2"><span class="text-var" data-bind="text: somevar">_</span></div></div>'
|
82
|
+
Element.find('body') << subview_html
|
83
|
+
view_model = TestSubviewBindings.new
|
84
|
+
expect(Element.find('#bindtest2 > div:eq(0) > span.text-var').text).to eq('1244500')
|
85
|
+
view_model.somesubview.somevar = 67890
|
86
|
+
expect(Element.find('#bindtest2 > div:eq(0) > span.text-var').text).to eq('6789000')
|
87
|
+
|
88
|
+
# at one point more than one subview was buggy. make sure this is working!!!
|
89
|
+
expect(Element.find('#bindtest2 > div:eq(1) > span.text-var').text).to eq('1254500')
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe 'bindings with view collections' do
|
94
|
+
it 'should bind a view with view collection objects' do
|
95
|
+
collection_html = '<div id="bindcoltest" data-bind-id="test-collection" style="display:none"><ul data-bind="foreach: items"><li><span data-bind="text: title">_</span></li></ul></div>'
|
96
|
+
Element.find('body') << collection_html
|
97
|
+
|
98
|
+
load_array = [{title: 'Name 1'}, {title: 'Name 2'}]
|
99
|
+
|
100
|
+
view_model = TestCollection.new(load_array)
|
101
|
+
|
102
|
+
expect(Element.find('#bindcoltest > ul > li:eq(0) > span').text).to eq('Name 1')
|
103
|
+
expect(Element.find('#bindcoltest > ul > li:eq(1) > span').text).to eq('Name 2')
|
104
|
+
|
105
|
+
view_model.items[0].title = "Name 1.0"
|
106
|
+
|
107
|
+
expect(Element.find('#bindcoltest > ul > li:eq(0) > span').text).to eq('Name 1.0')
|
108
|
+
|
109
|
+
view_model.items << {title: "Name 3"}
|
110
|
+
|
111
|
+
expect(Element.find('#bindcoltest > ul > li:eq(2) > span').text).to eq('Name 3')
|
112
|
+
|
113
|
+
expect(view_model.items[0].parent_view).to eq(view_model)
|
114
|
+
|
115
|
+
view_model.items << TestCollectionItem.new.tap{|item| item.parent_view = view_model; item.title = "Name 4" }
|
116
|
+
|
117
|
+
expect(Element.find('#bindcoltest > ul > li:eq(3) > span').text).to eq('Name 4')
|
118
|
+
end
|
119
|
+
end
|
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: polished-knockout
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jared White
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: opal
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: opal-jquery
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.4'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.5'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.5'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
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: opal-rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.4'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.4'
|
83
|
+
description: An Opal library for creating view models that use Knockout.js for dynamic
|
84
|
+
HTML updates and event handling
|
85
|
+
email:
|
86
|
+
- jared@jaredwhite.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- CODE_OF_CONDUCT.md
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- config.ru
|
98
|
+
- lib/polished-knockout.rb
|
99
|
+
- lib/polished/knockout.rb
|
100
|
+
- lib/polished/knockout/helpers/knockout_helpers.rb
|
101
|
+
- lib/polished/knockout/railtie.rb
|
102
|
+
- lib/polished/knockout/version.rb
|
103
|
+
- lib/polished/knockout/view_model.rb
|
104
|
+
- polished-knockout.gemspec
|
105
|
+
- spec/fixtures/users.json
|
106
|
+
- spec/knockout/index.html.erb
|
107
|
+
- spec/server_roundtrip_spec.rb
|
108
|
+
- spec/spec_helper.rb
|
109
|
+
- spec/view_model_spec.rb
|
110
|
+
homepage: https://github.com/polished-rb/knockout-rb
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
metadata: {}
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: '0'
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 2.4.5
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: An Opal library for creating view models that use Knockout.js for dynamic
|
134
|
+
HTML updates and event handling
|
135
|
+
test_files:
|
136
|
+
- spec/fixtures/users.json
|
137
|
+
- spec/knockout/index.html.erb
|
138
|
+
- spec/server_roundtrip_spec.rb
|
139
|
+
- spec/spec_helper.rb
|
140
|
+
- spec/view_model_spec.rb
|