knockout_forms-rails 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d733f242bb995739e41da161277091e2f707972
4
+ data.tar.gz: 9f1e588416553454d9d17e8224098982e5d8fa03
5
+ SHA512:
6
+ metadata.gz: 88754d82d026c355c610ef71bc6e13436fcf1770b8f7dd88820cf4bccdf7c8e7f2527a7f5c69093cf8f91214ba0d3aa695f3a28c05811b8d89239e2713146fac
7
+ data.tar.gz: df37feb8697a0bf138db112e177a8ad32ca3b82eddf91f807479e1bcaf131390ef20a99115694c57f6283aa5f85557424fa3e64ef5316157162cd2794c3c29f0
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in knockout_forms-rails.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Santiago Palladino
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.
@@ -0,0 +1,203 @@
1
+ # KnockoutForms::Rails
2
+
3
+ Knockoutjs powered form builder for Rails. This gem provides a `knockout_form_for` helper that creates forms automatically bound to a viewmodel created for your model via the mapping plugin.
4
+
5
+ ## Why?
6
+
7
+ There was a large gap between how Rails standard forms and knockoutjs based forms are written: where the former make use of handy form builders, the latter require the markdown the be manually set up and wired with `data-bind` attributes, even if the viewmodel is rather simple.
8
+
9
+ This gem intends to bridge this gap by allowing a Rails form to be easily converted in a knockoutjs aware one, by allowing you to keep using form builders while automatically handling data binding under the covers.
10
+
11
+ That being said, the gem provides several endpoints for customisation, making it easy to insert your custom logic whenever needed.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'knockout_forms-rails'
19
+ ```
20
+
21
+ And add the following directive to your Javascript manifest file (application.js):
22
+
23
+ ```javascript
24
+ //= require knockout-forms.rails
25
+ ```
26
+
27
+ ## Dependencies
28
+
29
+ This gem requires both knockoutjs and the knockoutjs mapping plugin. The easiest way to do this is via the [knockoutjs-rails gem](https://github.com/jswanner/knockoutjs-rails); though it is not listed as a dependency in case you want to manage your javascript libraries somehow else.
30
+
31
+ ## Example application
32
+
33
+ There is an [example application](https://github.com/spalladino/knockout_forms-rails-example) available with a simple [User form](https://github.com/spalladino/knockout_forms-rails-example/blob/master/app/views/users/_form.html.erb) and a complex [Questionnaire form](https://github.com/spalladino/knockout_forms-rails-example/blob/master/app/views/questionnaires/_form.html.erb) with a N-to-1 relation to Questions.
34
+
35
+ ## Usage
36
+
37
+ This gem provides a `KnockoutForms::Rails::FormBuilder` class, accessible via the `knockout_form_for` method, which must be bound invoking the javascript `ko.form(f)` method with a reference to it.
38
+
39
+ ### Basic usage
40
+
41
+ The easiest example is to use the `knockout_form_for` helper instead of a regular form, and invoke `ko.form()` on page load for the form you want to bind. For example, given a simple `user` model:
42
+
43
+ ```haml
44
+ = knockout_form_for(@user) do |f|
45
+
46
+ .form-group
47
+ = f.label :name
48
+ = f.text_field :name
49
+
50
+ .form-group
51
+ = f.label :registered
52
+ = f.check_box :registered
53
+
54
+ .form-group{"data-bind" => "visible: registered"}
55
+ = f.label :email
56
+ = f.text_field :email
57
+
58
+ .actions
59
+ = f.submit class: 'btn btn-primary'
60
+ ```
61
+
62
+ The `data-bind` with value `visible: registered` on the `email` field. This will display the email field only if the registered property is set to true.
63
+
64
+ The gem automatically creates a viewmodel for you based on the `user` model via the mapping plugin, bind it to the form, and set `value` bindings for all inputs.
65
+
66
+ To ensure `ko-forms` are always initialized, you can use this script:
67
+
68
+ ```coffeescript
69
+ $ ->
70
+ $('.ko-form').each ->
71
+ ko.form this
72
+ ```
73
+
74
+ ### Providing your own viewmodel
75
+
76
+ You can use your own viewmodel with your custom functions instead of an empty object. There are a few ways of doing this:
77
+
78
+ - The easiest way is to simple define a javascript class in the global scope with the same name as the model, which will be picked up by the `ko.form()` function:
79
+
80
+
81
+ class @Questionnaire
82
+ preview:
83
+ alert "Questionnaire: #{@title()}"
84
+
85
+
86
+ You can then invoke the `preview` function in your viewmodel class from `click` handler, or using the `action` method in the form builder:
87
+
88
+ = f.action 'Preview question', 'preview'
89
+
90
+ - The other option is to manually provide a reference to the class in the `ko.form()` method invocation:
91
+
92
+ class Questionnaire
93
+ preview:
94
+ alert "Questionnaire: #{@title()}"
95
+
96
+ $ ->
97
+ form = $('.questionnaire-ko-form')[0]
98
+ ko.form form, class: Questionnaire
99
+
100
+ - Alternatively, you can provide your own already populated viewmodel instance, in which case the `model` information will not be used and the mapping plugin will not be required. Just pass a `viewmodel` option to the `ko.form` function to do this:
101
+
102
+ questionnaire = new Questionnaire()
103
+ ko.form form, viewmodel: questionnaire
104
+
105
+ - Furthermore, you can skip using the `ko.form` function altogether and simply run a simple `ko.applyBindings` to the form using a viewmodel of your choice.
106
+
107
+ ### Mapping options
108
+
109
+ A custom mapping for your viewmodel can be provided as well. This will be passed directly as an argument to the `ko.mapping.fromJSON` invocation used to populate your viewmodel. It can be specified in the following ways:
110
+
111
+ - If you have supplied a viewmodel class and it has a `mapping` attribute, then it will be used automatically:
112
+
113
+ class Questionnaire
114
+ @mapping:
115
+ copy: ["description"]
116
+
117
+ - As an option to the `ko.form` method:
118
+
119
+ ko.form form, mapping: myCustomMapping
120
+
121
+ ### Model data
122
+
123
+ The model used to populate the knockout viewmodel is the result of a `to_json` call to the Rails model. If there are certain properties you want to include that are not ActiveRecord fields, you need to specify that when creating the form.
124
+
125
+ - Add the fields to be included as a `model_options` option in the `knockout_form_for` declaration:
126
+
127
+ model_options: { include: :custom_field }
128
+
129
+ - Or directly specify the model you want to be used via `model`:
130
+
131
+ model: @questionnaire.to_json({ include: :custom_field })
132
+
133
+ Keep in mind that the model information is not required if you specify your own viewmodel via the `viewmodel` option in the `ko.form` invocation.
134
+
135
+ ### Nested forms
136
+
137
+ Nested 1-to-N forms can be easily set up, and was one of the main drivers in the development of this gem. They do require some custom options to be properly handled, which are listed next. Refer to the questionnaire form (a questionnaire has_many questions) in the example application for a complete reference.
138
+
139
+ - Child elements must be included in the `model` when serializing it to populate the viewmodel, otherwise the children will not be included when editing an instance of the parent:
140
+
141
+ model_options: { include: :questions }
142
+
143
+ - In order to use custom viewmodel classes for both the parent and child elements, a custom mapping needs to be set up:
144
+
145
+ class @Question
146
+
147
+ class @Questionnaire
148
+ @mapping:
149
+ questions:
150
+ create: (opts) ->
151
+ ko.mapping.fromJS(opts.data, {}, new Question())
152
+
153
+ This will instruct the knockout mapping plugin to use a new instance of Question for each question in questionnaire that needs to be populated.
154
+
155
+ - The `fields_for` method in the form builder is overriden so it will use a `foreach` binding to display one template for each child. A `hidden_field` for the `id` must be included to keep track of existing instances.
156
+
157
+ = f.fields_for :questions do |g|
158
+
159
+ %h3 Question
160
+ = g.hidden_field :id
161
+
162
+ .form-group
163
+ = g.label :text
164
+ = g.text_field :text
165
+
166
+ Inside the `fields_for` body, all the markdown will be rendered once per question, and will be bound to the corresponding instance.
167
+
168
+ - A method `add_item` is provided in the form builder for convenience, which will attempt to automatically add an empty instance of the specified child viewmodel, populated from an empty child using the mapping plugin:
169
+
170
+ = f.add_item :questions, label: "Add new question", viewmodel: 'Question'
171
+
172
+ The usage of this helper is complete optional, as adding new items to the collection can be handled manually, by writing your own `addQuestion` function in the `Questionnaire` class and invoking it as:
173
+
174
+ = f.action "Add new question", 'addQuestion'
175
+
176
+ - As with all nested forms, remember to include the `accepts_nested_attibutes_for` option in your model, so the `fields_for` generated parameters will work correctly, which need to be permitted in the controller as well.
177
+
178
+ ### Using your own form builder
179
+
180
+ All methods in the form builder are provided in a mixin `KnockoutForms::Rails::FormBuilder::Methods` which you can include to your own FormBuilder to have it render knockout based forms.
181
+
182
+ ## Internals
183
+
184
+ The `KnockoutForms::Rails::FormBuilder` is the core of this gem. It overrides standard input helpers by adding a `data-bind` mapping to either their value or checked state, so they are automatically bound to the viewmodel attributes.
185
+
186
+ The input helpers currently supported are the following, you are welcome to send a pull request with your own:
187
+
188
+ - `text_field`
189
+ - `number_field `
190
+ - `hidden_field`
191
+ - `check_box `
192
+ - `radio_button`
193
+ - `select`
194
+
195
+ The counterpart of the form builder is the javascript code that binds it: the `knockout-forms.rails.js` script contains the `ko.form` function that either directly binds the form to the chosen viewmodel, or creates the viewmodel using the `model` JSON representation and the mapping plugin.
196
+
197
+ ## Contributing
198
+
199
+ 1. Fork it ( https://github.com/manastech/knockout_forms-rails/fork )
200
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
201
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
202
+ 4. Push to the branch (`git push origin my-new-feature`)
203
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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 'knockout_forms/rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "knockout_forms-rails"
8
+ spec.version = KnockoutForms::Rails::VERSION
9
+ spec.authors = ["Santiago Palladino"]
10
+ spec.email = ["spalladino@manas.com.ar"]
11
+ spec.summary = %q{Knockout-js powered Rails form builder}
12
+ spec.description = %q{Provides a Rails form builder to seamlessly setup a Knockout-js based view model on your forms.}
13
+ spec.homepage = ""
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_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+
24
+ spec.add_runtime_dependency "railties", [">= 3.1", "< 5"]
25
+ spec.add_runtime_dependency "actionview", [">= 3", "< 5"]
26
+ end
@@ -0,0 +1,33 @@
1
+ (function() {
2
+ var _global = this;
3
+ ko.form = function(target, opts) {
4
+
5
+ var options = opts || {},
6
+ form = target,
7
+ klazz = options.class || form.getAttribute('data-viewmodel'),
8
+ model = options.model || form.getAttribute('data-model'),
9
+ viewmodel,
10
+ mapping;
11
+
12
+ // Get constructor function based on name
13
+ if (typeof(klazz) == 'string') {
14
+ klazz = _global[klazz];
15
+ }
16
+
17
+ // Use viewmodel from options or create a new one
18
+ if (options.viewmodel) {
19
+ viewmodel = options.viewmodel;
20
+ } else if (klazz){
21
+ viewmodel = new klazz;
22
+ mapping = klazz.mapping || options.mapping || {};
23
+ ko.mapping.fromJSON(model, mapping, viewmodel);
24
+ } else {
25
+ mapping = options.mapping || {};
26
+ viewmodel = ko.mapping.fromJSON(model, mapping);
27
+ }
28
+
29
+ // Apply ko bindings
30
+ ko.applyBindings(viewmodel, target);
31
+
32
+ };
33
+ })(this);
@@ -0,0 +1,15 @@
1
+ require "knockout_forms/rails/version"
2
+ require "knockout_forms/rails/form_builder"
3
+ require "knockout_forms/rails/helpers/form_helper"
4
+
5
+ module KnockoutForms
6
+ module Rails
7
+ class Engine < ::Rails::Engine
8
+ initializer 'knockout_forms-rails.initialize' do
9
+ ActiveSupport.on_load(:action_view) do
10
+ include KnockoutForms::Rails::Helpers::FormHelper
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,82 @@
1
+ module KnockoutForms
2
+
3
+ module Rails
4
+
5
+ class FormBuilder < ActionView::Helpers::FormBuilder
6
+
7
+ module Methods
8
+
9
+ # Define attribute to bind based on input kind
10
+ MAPPINGS = {
11
+ value: %W(text_field number_field hidden_field),
12
+ checked: %W(check_box radio_button)
13
+ }
14
+
15
+ def self.included(form)
16
+ # Wrap all input fields so they add a KO value data bind
17
+ MAPPINGS.each do |bind, fields|
18
+ fields.each do |field_name|
19
+ form.send(:define_method, field_name) do |name, *args|
20
+ opts = args.extract_options!
21
+ opts['data-bind'] = "#{bind}: #{name}" unless opts.delete(:bind) == false
22
+ super(name, *(args << opts))
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # Handle select differently due to the html opts
29
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
30
+ html_options['data-bind'] = "value: #{method}" unless options.delete(:bind) == false
31
+ super(method, choices, options, html_options, &block)
32
+ end
33
+
34
+ def fields_for(collection_name, options={}, &block)
35
+ empty_child = options[:empty] || object.association(collection_name).klass.new
36
+ collection = options[:collection] || collection_name
37
+ # Run fields_for with a single empty child that will act as the KO template for each item
38
+ # and use foreach data bind to delegate the iteration to KO
39
+ @template.content_tag(:div,
40
+ super(collection_name, [empty_child], options.merge(child_index: ""), &block),
41
+ :'data-bind' => "foreach: #{collection}",
42
+ :'data-collection' => collection,
43
+ :'class' => "children-collection #{collection}-collection")
44
+ end
45
+
46
+ def add_item(collection_name, options={})
47
+ child_klass = object.association(collection_name).klass
48
+ empty_child = child_klass.new
49
+
50
+ label = options.delete(:label) || "Add new #{child_klass.model_name.singular}"
51
+ viewmodel_collection = options.delete(:collection) || collection_name
52
+ viewmodel = options.delete(:child_class) || child_klass.name
53
+
54
+ # Create an empty child to inject attributes via KO mapping
55
+ model = empty_child.to_json
56
+
57
+ # Create new child viewmodel augmented with model attributes and
58
+ # automatically add to viewmodel collection on click
59
+ click_handler = options[:handler] || <<-JS_HANDLER
60
+ function(data, event) {
61
+ var viewmodel = new #{viewmodel}(data);
62
+ ko.mapping.fromJS(#{model}, {}, viewmodel);
63
+ #{viewmodel_collection}.push(viewmodel);
64
+ };
65
+ JS_HANDLER
66
+
67
+ action(label, click_handler, options)
68
+ end
69
+
70
+ def action(label, action, options={})
71
+ @template.link_to(label, '#', options.merge('data-bind' => "click: #{action}"))
72
+ end
73
+
74
+ end
75
+
76
+ include Methods
77
+
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,17 @@
1
+ module KnockoutForms::Rails::Helpers
2
+ module FormHelper
3
+
4
+ def knockout_form_for(object, options={}, &block)
5
+ options[:builder] ||= KnockoutForms::Rails::FormBuilder
6
+
7
+ html = (options[:html] ||= {})
8
+
9
+ html[:'class'] ||= "ko-form"
10
+ html[:'data-model'] ||= (options.delete(:model) || object.to_json(options.delete(:model_options) || {}))
11
+ html[:'data-viewmodel'] ||= object.class.to_s
12
+
13
+ form_for(object, options, &block)
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ module KnockoutForms
2
+ module Rails
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knockout_forms-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Santiago Palladino
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-13 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.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: railties
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '3.1'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: '5'
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '3.1'
58
+ - - "<"
59
+ - !ruby/object:Gem::Version
60
+ version: '5'
61
+ - !ruby/object:Gem::Dependency
62
+ name: actionview
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3'
68
+ - - "<"
69
+ - !ruby/object:Gem::Version
70
+ version: '5'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '3'
78
+ - - "<"
79
+ - !ruby/object:Gem::Version
80
+ version: '5'
81
+ description: Provides a Rails form builder to seamlessly setup a Knockout-js based
82
+ view model on your forms.
83
+ email:
84
+ - spalladino@manas.com.ar
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files: []
88
+ files:
89
+ - ".gitignore"
90
+ - Gemfile
91
+ - LICENSE.txt
92
+ - README.md
93
+ - Rakefile
94
+ - knockout_forms-rails.gemspec
95
+ - lib/assets/javascripts/knockout-forms.rails.js
96
+ - lib/knockout_forms/rails.rb
97
+ - lib/knockout_forms/rails/form_builder.rb
98
+ - lib/knockout_forms/rails/helpers/form_helper.rb
99
+ - lib/knockout_forms/rails/version.rb
100
+ homepage: ''
101
+ licenses:
102
+ - MIT
103
+ metadata: {}
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubyforge_project:
120
+ rubygems_version: 2.2.2
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: Knockout-js powered Rails form builder
124
+ test_files: []