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
previewfunction in your viewmodel class fromclickhandler, or using theactionmethod 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: QuestionnaireAlternatively, you can provide your own already populated viewmodel instance, in which case the
modelinformation will not be used and the mapping plugin will not be required. Just pass aviewmodeloption to theko.formfunction to do this:questionnaire = new Questionnaire() ko.form form, viewmodel: questionnaireFurthermore, you can skip using the
ko.formfunction altogether and simply run a simpleko.applyBindingsto 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
mappingattribute, then it will be used automatically:class Questionnaire @mapping: copy: ["description"]As an option to the
ko.formmethod: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_optionsoption in theknockout_form_fordeclaration: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
modelwhen 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_formethod in the form builder is overriden so it will use aforeachbinding to display one template for each child. Ahidden_fieldfor theidmust 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 :textInside the
fields_forbody, all the markdown will be rendered once per question, and will be bound to the corresponding instance.A method
add_itemis 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
addQuestionfunction in theQuestionnaireclass and invoking it as:= f.action "Add new question", 'addQuestion'As with all nested forms, remember to include the
accepts_nested_attibutes_foroption in your model, so thefields_forgenerated 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_fieldnumber_fieldhidden_fieldcheck_boxradio_buttonselect
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