KnockoutForms::Rails
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.
Why?
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.
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.
That being said, the gem provides several endpoints for customisation, making it easy to insert your custom logic whenever needed.
Installation
Add this line to your application's Gemfile:
gem 'knockout_forms-rails'
And add the following directive to your Javascript manifest file (application.js):
//= require knockout-forms.rails
Dependencies
This gem requires both knockoutjs and the knockoutjs mapping plugin. The easiest way to do this is via the knockoutjs-rails gem; though it is not listed as a dependency in case you want to manage your javascript libraries somehow else.
Example application
There is an example application available with a simple User form and a complex Questionnaire form with a N-to-1 relation to Questions.
Usage
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.
Basic usage
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:
= knockout_form_for(@user) do |f|
.form-group
= f.label :name
= f.text_field :name
.form-group
= f.label :registered
= f.check_box :registered
.form-group{"data-bind" => "visible: registered"}
= f.label :email
= f.text_field :email
.actions
= f.submit class: 'btn btn-primary'
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.
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.
To ensure ko-forms
are always initialized, you can use this script:
$ ->
$('.ko-form').each ->
ko.form this
Providing your own viewmodel
You can use your own viewmodel with your custom functions instead of an empty object. There are a few ways of doing this:
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:class @Questionnaire preview: alert "Questionnaire: #{@title()}"
You can then invoke the
preview
function in your viewmodel class fromclick
handler, or using theaction
method in the form builder:= f.action 'Preview question', 'preview'
The other option is to manually provide a reference to the class in the
ko.form()
method invocation:class Questionnaire preview: alert "Questionnaire: #{@title()}" $ -> form = $('.questionnaire-ko-form')[0] ko.form form, class: Questionnaire
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 aviewmodel
option to theko.form
function to do this:questionnaire = new Questionnaire() ko.form form, viewmodel: questionnaire
Furthermore, you can skip using the
ko.form
function altogether and simply run a simpleko.applyBindings
to the form using a viewmodel of your choice.
Mapping options
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:
If you have supplied a viewmodel class and it has a
mapping
attribute, then it will be used automatically:class Questionnaire @mapping: copy: ["description"]
As an option to the
ko.form
method:ko.form form, mapping: myCustomMapping
Model data
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.
Add the fields to be included as a
model_options
option in theknockout_form_for
declaration:model_options: { include: :custom_field }
Or directly specify the model you want to be used via
model
:model: @questionnaire.to_json({ include: :custom_field })
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.
Nested forms
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.
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:model_options: { include: :questions }
In order to use custom viewmodel classes for both the parent and child elements, a custom mapping needs to be set up:
class @Question class @Questionnaire @mapping: questions: create: (opts) -> ko.mapping.fromJS(opts.data, {}, new Question())
This will instruct the knockout mapping plugin to use a new instance of Question for each question in questionnaire that needs to be populated.
The
fields_for
method in the form builder is overriden so it will use aforeach
binding to display one template for each child. Ahidden_field
for theid
must be included to keep track of existing instances.= f.fields_for :questions do |g| %h3 Question = g.hidden_field :id .form-group = g.label :text = g.text_field :text
Inside the
fields_for
body, all the markdown will be rendered once per question, and will be bound to the corresponding instance.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:= f.add_item :questions, label: "Add new question", viewmodel: 'Question'
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 theQuestionnaire
class and invoking it as:= f.action "Add new question", 'addQuestion'
As with all nested forms, remember to include the
accepts_nested_attibutes_for
option in your model, so thefields_for
generated parameters will work correctly, which need to be permitted in the controller as well.
Using your own form builder
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.
Internals
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.
The input helpers currently supported are the following, you are welcome to send a pull request with your own:
text_field
number_field
hidden_field
check_box
radio_button
select
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.
Contributing
- Fork it ( https://github.com/manastech/knockout_forms-rails/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request