├── CHANGELOG.md ├── MIT-LICENSE ├── README.md ├── Rakefile ├── VERSION ├── lib ├── vue-rails-form-builder.rb └── vue-rails-form-builder │ ├── form_builder.rb │ ├── form_helper.rb │ ├── railtie.rb │ ├── tag_helper.rb │ └── vue_options_resolver.rb └── vue-rails-form-builder.gemspec /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG - vue-rails-form-builder 2 | 3 | ## 0.8.2 (2018-02-15) 4 | 5 | * Do override the `file_filed` helper to resolve vue.js options. 6 | 7 | ## 0.8.1 (2018-02-15) 8 | 9 | * Don't override the `file_filed` helper. Fix #3. 10 | 11 | ## 0.8.0 (2017-11-11) 12 | 13 | * Add `vue_prefix` method to the form builder. 14 | 15 | ## 0.7.0 (2017-11-11) 16 | 17 | * Support nested attributes. 18 | 19 | ## 0.6.0 (2017-05-14) 20 | 21 | * Add `vue_scope` option to `vue_form_for` and `vue_form_with` methods. 22 | 23 | ## 0.5.0 (2017-05-12) 24 | 25 | * Change the name of this gem to `vue-rails-form-builder` from `vue-form-for`. 26 | 27 | ## 0.4.0 (2017-05-11) 28 | 29 | * Support SELECT tag. 30 | 31 | ## 0.3.3 (2017-05-09) 32 | 33 | * Generate v-model attr for check boxes and radio buttons. 34 | 35 | ## 0.3.2 (2017-05-09) 36 | 37 | * Generate correct value for v-model on nested form. 38 | 39 | ## 0.3.1 (2017-04-11) 40 | 41 | * Fix `check_box` and `radio_button` helper methods. 42 | 43 | ## 0.3.0 (2017-04-09) 44 | 45 | * Add `vue_tag` and `vue_content_tag` helper methods 46 | that can handle `:bind`, `:on`, `:checked`, `:disabled`, `:multiple`, 47 | `:readonly`, `:selected`, `:text`, `:html`, `:show`, `:if`, `:else`, 48 | `:else_if`, `:for`, `:model`, `:pre`, `:cloak`, `:once` options. 49 | * Add similar functionalities to the form building helpers. 50 | * Remove `:v` option handling. 51 | 52 | ## 0.2.0 (2017-04-07) 53 | 54 | * Allow the form builder to resolve `:v` option. 55 | 56 | ## 0.1.0 (2017-04-05) 57 | 58 | * The first release with minimum functionalities. 59 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Tsutomu Kuroda 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vue-rails-form-builder 2 | ====================== 3 | 4 | [](https://badge.fury.io/rb/vue-rails-form-builder) 5 | 6 | A custom Rails form builder for Vue.js 7 | 8 | Synopsis 9 | -------- 10 | 11 | ```erb 12 | <%= vue_form_for User.new do |f| %> 13 | <%= f.text_field :name %> 14 | <% end %> 15 | ``` 16 | 17 | ```html 18 |
22 | ``` 23 | 24 | Installation 25 | ------------ 26 | 27 | Add the following line to `Gemfile`: 28 | 29 | ```ruby 30 | gem "vue-rails-form-builder" 31 | ``` 32 | 33 | Run `bundle install` on the terminal. 34 | 35 | Usage 36 | ----- 37 | 38 | ```erb 39 | <%= vue_form_for User.new do |f| %> 40 | <%= f.text_field :name %> 41 | <%= f.submit "Create" %> 42 | <% end %> 43 | ``` 44 | 45 | The above ERB template will be rendered into the following HTML fragment: 46 | 47 | ```html 48 | 54 | ``` 55 | 56 | Note that the third `` element has a `v-model` attriubte, which can be 57 | interpreted by Vue.js as the _directive_ to create two-way data bindings between 58 | form fields and component's data. 59 | 60 | If you are using the [Webpacker](https://github.com/rails/webpacker), 61 | create `app/javascript/packs/new_user_form.js` with following code: 62 | 63 | ```javascript 64 | import Vue from 'vue/dist/vue.esm' 65 | 66 | document.addEventListener("DOMContentLoaded", () => { 67 | const NewUserForm = new Vue({ 68 | el: "#new_user", 69 | data: { 70 | user: { 71 | name: "" 72 | } 73 | } 74 | }) 75 | }) 76 | ``` 77 | 78 | Add this line to the ERB template: 79 | 80 | ```erb 81 | <%= javascript_pack_tag "new_user_form" %> 82 | ``` 83 | 84 | Then, you can get the value of `user[name]` field by the `user.name`. 85 | 86 | If you use Rails 5.1 or above, you can also use `vue_form_with`: 87 | 88 | ```erb 89 | <%= vue_form_with model: User.new do |f| %> 90 | <%= f.text_field :name %> 91 | <%= f.submit "Create" %> 92 | <% end %> 93 | ``` 94 | 95 | Demo App 96 | -------- 97 | 98 | Visit [vue-rails-form-builder-demo](https://github.com/kuroda/vue-rails-form-builder-demo) 99 | for a working Rails demo application using the `vue-rails-form-builder`. 100 | 101 | Options 102 | ------- 103 | 104 | To `vue_form_for` and `vue_form_with` methods you can provide the same options 105 | as `form_for` and `form_with`. 106 | 107 | There is a special option: 108 | 109 | * `:vue_scope` - The prefix used to the input field names within the Vue 110 | component. 111 | 112 | Tag Helper 113 | ---------- 114 | 115 | This gem provides two additional helper methods: `vue_tag` and `vue_content_tag`. 116 | 117 | Basically, they behave like `tag` and `content_tag` helpers of Action Views. 118 | But, they interpret the *HTML options* in a different way as explained below. 119 | 120 | ### The `:bind` option 121 | 122 | If the *HTML options* have a `:bind` key and its value is a hash, 123 | they get transformed into the Vue.js `v-bind` directives. 124 | 125 | In the example below, these two lines have the same result: 126 | 127 | ```erb 128 | <%= vue_content_tag(:span, "Hello", bind: { style: "{ color: textColor }" }) %> 129 | <%= vue_content_tag(:span, "Hello", "v-bind:style" => "{ color: textColor }" }) %> 130 | ``` 131 | 132 | Note that you should use the latter style if you want to specify *modifiers* 133 | to the `v-bind` directives. For example: 134 | 135 | ```erb 136 | <%= vue_content_tag(:span, "Hello", "v-bind:text-content.prop" => "message" }) %> 137 | ``` 138 | 139 | ### The `:on` option 140 | 141 | If the *HTML options* have a `:on` key and its value is a hash, 142 | they get transformed into the Vue.js `v-on` directives. 143 | 144 | In the example below, these two lines have the same result: 145 | 146 | ```erb 147 | <%= vue_content_tag(:span, "Hello", on: { click: "doThis" }) %> 148 | <%= vue_content_tag(:span, "Hello", "v-on:click" => "doThis" }) %> 149 | ``` 150 | 151 | Note that you should use the latter style if you want to specify *modifiers* 152 | to the `v-on` directives. For example: 153 | 154 | ```erb 155 | <%= vue_content_tag(:span, "Hello", "v-on:click.once" => "doThis" }) %> 156 | <%= vue_content_tag(:button, "Hello", "v-on:click.prevent" => "doThis" }) %> 157 | ``` 158 | 159 | ### Boolean attributes 160 | 161 | If the *HTML options* have a string value (not a boolean value) 162 | for `checked`, `disabled`, `multiple`, `readonly` or `selected` key, 163 | the key gets transformed by adding `v-bind:` to its head. 164 | 165 | In the example below, these two lines have the same result: 166 | 167 | ```erb 168 | <%= vue_content_tag(:button, "Click me!", disabled: "!clickable") %> 169 | <%= vue_content_tag(:button, "Click me!", "v-bind:disabled" => "!clickable") %> 170 | ``` 171 | 172 | If you want to add a normal attribute without `v-bind:` prefix, 173 | specify `true` (boolean) to these keys: 174 | 175 | ```erb 176 | <%= vue_content_tag(:button, "Click me!", disabled: true) %> 177 | ``` 178 | 179 | This line produces the following HTML fragment: 180 | 181 | ```html 182 | 183 | ``` 184 | 185 | 186 | ### Vue.js directives 187 | 188 | If the *HTML options* have one or more of the following keys 189 | 190 | > `text`, `html`, `show`, `if`, `else`, `else_if`, `for`, `model`, `pre`, `cloak`, `once` 191 | 192 | then, these keys get transformed by adding `v-` to their head. 193 | 194 | In the example below, these two lines have the same result: 195 | 196 | ```erb 197 | <%= vue_tag(:hr, if: "itemsPresent") %> 198 | <%= vue_tag(:hr, "v-if" => "itemsPresent") %> 199 | ``` 200 | 201 | Note that the `:else_if` key is transformed into the `v-else-if` directive: 202 | 203 | ```erb 204 | <%= vue_tag(:hr, else_if: "itemsPresent") %> 205 | <%= vue_tag(:hr, "v-else-if" => "itemsPresent") %> 206 | ``` 207 | 208 | ### Extensions to the form building helpers 209 | 210 | When you build HTML forms using `vue_form_for`, 211 | the form building helpers, such as `text_field`, `check_box`, etc., 212 | have these additional behavior. 213 | 214 | Example: 215 | 216 | ```erb 217 | <%= vue_form_for User.new do |f| %> 218 | <%= f.text_field :name, model: "userName" %> 219 | 222 | <%= f.submit "Create", disabled: "!submittable" %> 223 | <% end %> 224 | ``` 225 | 226 | The `vue_prefix` method of the Form Builder 227 | ------------------------------------------- 228 | 229 | When you build HTML forms using `vue_form_for`, the form builder has the 230 | `vue_prefix` method that returns the *prefix string* to the Vue.js property names. 231 | 232 | See the following code: 233 | 234 | ```erb 235 | <%= vue_form_for User.new do |f| %> 236 | <%= f.text_field :name %> 237 | <%= f.submit "Create", disabled: "user.name === ''" %> 238 | <% end %> 239 | ``` 240 | 241 | The `vue_prefix` method of the form builder (`f`) returns the string `"user"` 242 | so that you can rewrite the third line of the example above like this: 243 | 244 | ```erb 245 | <%= f.submit "Create", disabled: "#{f.vue_prefix}.name === ''" %> 246 | ``` 247 | 248 | This method is convenient especially when the form has nested attributes: 249 | 250 | ```erb 251 | <%= vue_form_for @user do |f| %> 252 | <%= f.text_field :name %> 253 | <%= f.fields_for :emails do |g| %> 254 | <%= g.text_field :address, 255 | disabled: "user.emails_attributes[#{g.index}]._destroy" %> 256 | <%= g.check_box :_destroy if g.object.persisted? %> 257 | <% end %> 258 | <%= f.submit "Create", disabled: "#{f.vue_prefix}.name === ''" %> 259 | <% end %> 260 | ``` 261 | 262 | Using the `vue_prefix` method, you can rewrite the fifth line more concisely: 263 | 264 | ```erb 265 | disabled: g.vue_prefix + "._destroy" %> 266 | ``` 267 | 268 | Data Initialization 269 | ------------------- 270 | 271 | As the official Vue.js document says: 272 | 273 | > `v-model` will ignore the initial `value`, `checked` or `selected` attributes 274 | > found on any form elements. 275 | > (https://vuejs.org/v2/guide/forms.html) 276 | 277 | Because of this, all form controls get reset after the Vue component is mounted. 278 | 279 | However, you can use 280 | [vue-data-scooper](https://github.com/kuroda/vue-data-scooper) plugin 281 | in order to keep the original state of the form. 282 | 283 | License 284 | ------- 285 | 286 | The `vue-rails-form-builder` is distributed under the MIT license. ([MIT-LICENSE](https://github.com/kuroda/vue-rails-form-builder/blob/master/MIT-LICENSE)) 287 | 288 | Author 289 | ------ 290 | 291 | Tsutomu Kuroda (t-kuroda@oiax.jp) 292 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.8.3 2 | -------------------------------------------------------------------------------- /lib/vue-rails-form-builder.rb: -------------------------------------------------------------------------------- 1 | require "vue-rails-form-builder/form_builder" 2 | require "vue-rails-form-builder/railtie" 3 | -------------------------------------------------------------------------------- /lib/vue-rails-form-builder/form_builder.rb: -------------------------------------------------------------------------------- 1 | require_relative "./vue_options_resolver" 2 | 3 | module VueRailsFormBuilder 4 | class FormBuilder < ActionView::Helpers::FormBuilder 5 | include VueRailsFormBuilder::VueOptionsResolver 6 | 7 | (field_helpers - [:label, :check_box, :radio_button, :fields_for, :file_field]) 8 | .each do |selector| 9 | class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 10 | def #{selector}(method, options = {}) 11 | resolve_vue_options(options) 12 | add_v_model_attribute(method, options) 13 | super(method, options) 14 | end 15 | RUBY_EVAL 16 | end 17 | 18 | def label(method, text = nil, options = {}, &block) 19 | resolve_vue_options(options) 20 | super(method, text, options, &block) 21 | end 22 | 23 | def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") 24 | resolve_vue_options(options) 25 | add_v_model_attribute(method, options) 26 | super(method, options, checked_value, unchecked_value) 27 | end 28 | 29 | def radio_button(method, tag_value, options = {}) 30 | resolve_vue_options(options) 31 | add_v_model_attribute(method, options) 32 | super(method, tag_value, options) 33 | end 34 | 35 | def select(method, choices = nil, options = {}, html_options = {}, &block) 36 | resolve_vue_options(html_options) 37 | add_v_model_attribute(method, html_options) 38 | super(method, choices, options, html_options, &block) 39 | end 40 | 41 | def file_field(method, options = {}) 42 | resolve_vue_options(options) 43 | super(method, options) 44 | end 45 | 46 | def submit(value = nil, options = {}) 47 | resolve_vue_options(options) 48 | super(value, options) 49 | end 50 | 51 | def button(value = nil, options = {}, &block) 52 | resolve_vue_options(options) 53 | super(value, options, &block) 54 | end 55 | 56 | def vue_prefix 57 | path = @object_name.gsub(/\[/, ".").gsub(/\]/, "").split(".") 58 | if @options[:vue_scope] 59 | path[0] = @options[:vue_scope] 60 | end 61 | path.join(".").gsub(/\.(\d+)/, '[\1]') 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/vue-rails-form-builder/form_helper.rb: -------------------------------------------------------------------------------- 1 | module VueRailsFormBuilder 2 | module FormHelper 3 | def vue_form_with(**options) 4 | unless respond_to?(:form_with) 5 | raise "Your Rails does not implement form_with helper." 6 | end 7 | 8 | options[:builder] ||= VueRailsFormBuilder::FormBuilder 9 | if block_given? 10 | form_with(options, &Proc.new) 11 | else 12 | form_with(options) 13 | end 14 | end 15 | 16 | def vue_form_for(record, options = {}, &block) 17 | options[:builder] ||= VueRailsFormBuilder::FormBuilder 18 | form_for(record, options, &block) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/vue-rails-form-builder/railtie.rb: -------------------------------------------------------------------------------- 1 | require "vue-rails-form-builder/form_helper" 2 | require "vue-rails-form-builder/tag_helper" 3 | 4 | module VueRailsFormBuilder 5 | class Railtie < Rails::Railtie 6 | initializer "vue-rails-form-builder.view_form_helper" do 7 | ActiveSupport.on_load :action_view do 8 | include VueRailsFormBuilder::FormHelper 9 | include VueRailsFormBuilder::TagHelper 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/vue-rails-form-builder/tag_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative "./vue_options_resolver" 2 | 3 | module VueRailsFormBuilder 4 | module TagHelper 5 | include VueRailsFormBuilder::VueOptionsResolver 6 | 7 | def vue_tag(name, options = nil, open = false, escape = true) 8 | resolve_vue_options(options) if options 9 | tag(name, options, open, escape) 10 | end 11 | 12 | def vue_content_tag(name, content_or_options_with_block = nil, options = nil, 13 | escape = true, &block) 14 | 15 | if block_given? 16 | if content_or_options_with_block.is_a?(Hash) 17 | resolve_vue_options(content_or_options_with_block) 18 | end 19 | else 20 | resolve_vue_options(options) if options 21 | end 22 | 23 | content_tag(name, content_or_options_with_block, options, escape, &block) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/vue-rails-form-builder/vue_options_resolver.rb: -------------------------------------------------------------------------------- 1 | module VueRailsFormBuilder 2 | module VueOptionsResolver 3 | private def resolve_vue_options(options) 4 | if options[:bind].kind_of?(Hash) 5 | h = options.delete(:bind) 6 | h.each do |key, value| 7 | if value.kind_of?(String) 8 | options[:"v-bind:#{key}"] = value 9 | end 10 | end 11 | end 12 | 13 | if options[:on].kind_of?(Hash) 14 | h = options.delete(:on) 15 | h.each do |key, value| 16 | if value.kind_of?(String) 17 | options[:"v-on:#{key}"] = value 18 | end 19 | end 20 | end 21 | 22 | %i(checked disabled multiple readonly selected).each do |attr_name| 23 | if options[attr_name].kind_of?(String) 24 | options[:"v-bind:#{attr_name}"] = options.delete(attr_name) 25 | end 26 | end 27 | 28 | %i(text html show if else else_if for model).each do |directive| 29 | if options[directive].kind_of?(String) 30 | options[:"v-#{directive.to_s.dasherize}"] = options.delete(directive) 31 | end 32 | end 33 | 34 | %i(pre cloak once).each do |directive| 35 | if options[directive] 36 | options.delete(directive) 37 | options[:"v-#{directive}"] = "v-#{directive}" 38 | end 39 | end 40 | end 41 | 42 | private def add_v_model_attribute(method, options) 43 | path = @object_name.gsub(/\[/, ".").gsub(/\]/, "").split(".") 44 | if @options[:vue_scope] 45 | path[0] = @options[:vue_scope] 46 | end 47 | options[:"v-model"] ||= (path + [ method ]).join(".") 48 | options[:"v-model"].gsub!(/\.(\d+)/, '[\1]') 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /vue-rails-form-builder.gemspec: -------------------------------------------------------------------------------- 1 | version = File.read(File.expand_path("../VERSION",__FILE__)).strip 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "vue-rails-form-builder" 5 | s.version = version 6 | s.authors = [ "Tsutomu KURODA" ] 7 | s.email = "t-kuroda@oiax.jp" 8 | s.homepage = "https://github.com/kuroda/vue-rails-form-builder" 9 | s.description = "This gem provides four view helpers for Rails app: " + 10 | "vue_form_with, vue_form_for, vue_tag and vue_content_tag." 11 | s.summary = "A custom Rails form builder for Vue.js" 12 | s.license = "MIT" 13 | 14 | s.required_ruby_version = ">= 2.2.2" 15 | s.add_dependency "actionview", ">= 4.2", "< 7" 16 | s.add_dependency "railties", ">= 4.2", "< 7" 17 | 18 | s.files = %w(CHANGELOG.md README.md MIT-LICENSE) + Dir.glob("lib/**/*") 19 | end 20 | --------------------------------------------------------------------------------