webface_rails 0.1.6

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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +37 -0
  4. data/Rakefile +7 -0
  5. data/app/controllers/webface_error_report_controller.rb +12 -0
  6. data/app/helpers/webface_component_helper.rb +248 -0
  7. data/app/helpers/webface_form.rb +161 -0
  8. data/app/views/webface_component_templates/_button.html.haml +2 -0
  9. data/app/views/webface_component_templates/_dialog_window.html.haml +6 -0
  10. data/app/views/webface_component_templates/_modal_window.html.haml +5 -0
  11. data/app/views/webface_component_templates/_simple_notification.html.haml +4 -0
  12. data/app/views/webface_components/_button.html.haml +11 -0
  13. data/app/views/webface_components/_checkbox.html.haml +21 -0
  14. data/app/views/webface_components/_editable_select.html.haml +8 -0
  15. data/app/views/webface_components/_field_hints.html.haml +7 -0
  16. data/app/views/webface_components/_hidden_form_field.html.haml +1 -0
  17. data/app/views/webface_components/_hint.html.haml +9 -0
  18. data/app/views/webface_components/_hint_trigger.html.haml +15 -0
  19. data/app/views/webface_components/_numeric_form_field.html.haml +13 -0
  20. data/app/views/webface_components/_password_form_field.html.haml +12 -0
  21. data/app/views/webface_components/_post_button_link.html.haml +4 -0
  22. data/app/views/webface_components/_radio.html.haml +18 -0
  23. data/app/views/webface_components/_select.html.haml +34 -0
  24. data/app/views/webface_components/_text_form_field.html.haml +12 -0
  25. data/app/views/webface_components/_text_form_field_with_validation.html.haml +6 -0
  26. data/app/views/webface_components/_textarea_form_field.html.haml +11 -0
  27. data/app/views/webface_components/shared/_editable_select_base.html.haml +34 -0
  28. data/lib/generators/templates/application.js +53 -0
  29. data/lib/generators/templates/mocha.css +10 -0
  30. data/lib/generators/templates/mocha.pug +15 -0
  31. data/lib/generators/templates/my_component.test.js +15 -0
  32. data/lib/generators/templates/run_webface_test +11 -0
  33. data/lib/generators/templates/test_animator.js +66 -0
  34. data/lib/generators/templates/test_utils.js +6 -0
  35. data/lib/generators/templates/webface.test.js +2 -0
  36. data/lib/generators/templates/webface_init.js +10 -0
  37. data/lib/generators/templates/webface_test_server.js +68 -0
  38. data/lib/generators/webface_generator.rb +81 -0
  39. data/lib/tasks/webface_rails_tasks.rake +3 -0
  40. data/lib/webface_rails.rb +11 -0
  41. data/lib/webface_rails/version.rb +3 -0
  42. data/spec/helpers/webface_component_helper_spec.rb +133 -0
  43. data/spec/helpers/webface_form_spec.rb +58 -0
  44. data/spec/spec_helper.rb +15 -0
  45. metadata +147 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1cc42fc92095e740deb07c81294310846908ebd80d435bc1bc0873578185c978
4
+ data.tar.gz: 690639b7c31b20ca0e9ccda60410a6629e495d1bdecc3f05fb3a0cd7ad8dff4f
5
+ SHA512:
6
+ metadata.gz: 1fd031041de3a7740a6639ce52b45832399ea462766382b93d3237fb51b1ead330cdeb361cf97274850e91c81444eeb7ca2e5465042d9b8ee81915307fc00503
7
+ data.tar.gz: c9b256dde5c1acc4f734598ba188df1b0f99f2ef1da5196386b40b23b906cd11269a4d478db2edd0dea8ab78426a4cb31ffd612fabf9a873bed4d1c4a5b8bcaf
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Roman Snitko
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.
@@ -0,0 +1,37 @@
1
+ # WebfaceRails
2
+ RubyOnRails integration with Webface.js
3
+
4
+ Installation
5
+ ------------
6
+ add `gem 'webface_rails'` to Gemfile and run `bundle install`
7
+
8
+ Running generators
9
+ ------------------
10
+ To run the generator:
11
+
12
+ `rails generate webface` or `rails g webface`
13
+
14
+ By default it uses `Rails.root` path to install the gem. You can pass `root_path` argument to redefine root path in your project. It might be helpful when installing the gem into Rails Engine, e.g.:
15
+
16
+ `rails g webface --root_path absolute_path_to_your_project`
17
+
18
+ The generator will give you necessary files and dir structure inside the `spec/` dir to
19
+ run webface tests. To be more specific, it will do the following
20
+
21
+ 1. Install webface as a submodule under `app/assets/javascripts/webface.js`. If you already have it installed, it will use the path specified in .gitmodules
22
+ 2. Create webface unit test dir under `spec/webface`
23
+ 3. Copy there a bunch of files necessary to run unit tests
24
+ 4. Add `node_modules` dirs to .gitignore
25
+ 5. Symlink your `app/assets/javascripts` into `spec/webface/source` and `app/assets/javascripts` into `spec/webface/webface_source`. This is necessary to properly import the files you're testing.
26
+ 6. Install node_modules in `app/assets/javascripts`. If it doesn't have a `package.json` file, it will copy the one used by webface.
27
+ 7. Symlink `app/assets/javascripts/node_modules` to `spec/webface/node_modules`
28
+ 8. And finally, symlink the `app/assets/javascripts/webface.js` (or whatever the path is) into `spec/webface/webface_source`
29
+
30
+ Running unit tests
31
+ ------------------
32
+ After you run the generators, you should be able to run unit tests with
33
+ a) `spec/webface/run_test` or
34
+ b) by launching the test server with `node spec/webface/test_server.js` and navigating your browser to `localhost:8080`. You should see a sample test there.
35
+
36
+ Look at the `spec/webface/components/my_component.test.js` file for examples on how to write unit tests.
37
+ Unit tests are written with mocha & chai.
@@ -0,0 +1,7 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'bundler/gem_tasks'
@@ -0,0 +1,12 @@
1
+ class WebfaceErrorReportController < ApplicationController
2
+
3
+ def report
4
+ puts "**********************************"
5
+ puts "Frontend error detected. Please make sure you redefine this action\n" +
6
+ "to actually report this error to something like Sentry!\n" +
7
+ "below are the params that were sent by Webface Logmaster:"
8
+ p params
9
+ puts "**********************************"
10
+ end
11
+
12
+ end
@@ -0,0 +1,248 @@
1
+ module WebfaceComponentHelper
2
+
3
+ def webface(tag_name, component: nil, attrs: {})
4
+
5
+ component = "#{component.to_s.camelize}Component" unless component.nil?
6
+
7
+ if attrs[:property_attr]
8
+ property_attrs = attrs[:property_attr].split(/,\s*?/)
9
+ property_attrs.map! { |a| "#{a.strip}:data-#{a.strip.gsub("_","-")}" }
10
+ attrs[:property_attr] = property_attrs.join(", ")
11
+ end
12
+
13
+ # convert short data attribute names to proper longer ones
14
+ webface_data = {}
15
+ webface_data[:component_class] = component
16
+ webface_data[:component_property] = attrs.delete(:property)
17
+ webface_data[:component_part] = attrs.delete(:part)
18
+ webface_data[:component_roles] = attrs.delete(:roles)
19
+ webface_data[:component_attribute_properties] = attrs.delete(:property_attr)
20
+ webface_data[:component_display_value] = attrs.delete(:display)
21
+
22
+ content_tag(tag_name, { data: webface_data.merge(attrs.delete(:data) || {})}.merge(attrs)) do
23
+ yield if block_given?
24
+ end
25
+ end
26
+
27
+ def component(name, attrs={}, &block)
28
+ render partial: "webface_components/#{name}", locals: { options: attrs.delete(:options) || {}, attrs: attrs, block: block }
29
+ end
30
+
31
+ def component_block(component, tag_name="div", attrs={}, &block)
32
+ webface(tag_name, component: component, attrs: attrs, &block)
33
+ end
34
+
35
+ def property(property, tag_name="span", attrs={}, &block)
36
+ webface(tag_name, attrs: attrs.merge({ property: property}), &block)
37
+ end
38
+
39
+ def component_part(part_name, tag_name="div", attrs={}, &block)
40
+ webface(tag_name, attrs: attrs.merge({ part: part_name}), &block)
41
+ end
42
+
43
+ def button_link(caption, path, options={})
44
+ component_block (options[:confirmation] ? "confirmable_button" : "button"), "a", { class: "button", href: path, data: { prevent_native_click_event: "false", component_attribute_propertie: "lockable,disabled,confirmation,prevent_native_click_event", component_property: "caption" }}.merge(options) do
45
+ caption
46
+ end
47
+ end
48
+
49
+ def component_form(model, action=nil, attrs={}, &block)
50
+
51
+ if model.new_record?
52
+ action = send("#{model.class.to_s.underscore.pluralize}_path") unless action
53
+ method_field = ""
54
+ else
55
+ action = send("#{model.class.to_s.underscore}_path", model.to_param) unless action
56
+ method_field = content_tag(:input, "", type: "hidden", name: "_method", value: "PATCH")
57
+ end
58
+
59
+ method_field = content_tag(:input, "", type: "hidden", name: "_method", value: attrs.delete(:method)) if attrs[:method]
60
+
61
+ auth_token_field = content_tag(:input, "", value: form_authenticity_token, type: "hidden", name: "authenticity_token")
62
+
63
+ f = WebfaceForm.new(model, self)
64
+ content = capture(f, &block)
65
+ content_tag(:form, { action: action, method: 'POST', "accept-charset" => "UTF-8", "enctype" => "multipart/form-data" }.merge(attrs)) do
66
+ concat content
67
+ concat method_field
68
+ concat auth_token_field
69
+ end
70
+ end
71
+
72
+ def post_button_link(caption, path, verb, options={})
73
+ render partial: "webface_components/post_button_link", locals: { caption: caption, path: path, verb: verb, options: options }
74
+ end
75
+
76
+ # Attention! This method changes attrs passed to it,
77
+ # as if `attrs` was passed by reference! That's why we use `eval` and
78
+ # binding.
79
+ def data_attrs_and_values!(attrs, bdg, names=[])
80
+ data_hash = {}
81
+ names.each do |n|
82
+ data_hash[n] = eval("attrs.delete(:#{n})", bdg)
83
+ end
84
+ data_hash
85
+ end
86
+
87
+ def component_template(name, tag_name="div", attrs={}, &block)
88
+ attrs[:data] ||= {}
89
+ attrs[:data][:component_template] = "#{name.to_s.camelize}Component"
90
+ component_block(nil, tag_name, attrs, &block)
91
+ end
92
+
93
+ def prepare_select_collection(c, selected: nil, blank_option: false)
94
+
95
+ if c
96
+ collection = c.dup
97
+ else
98
+ return []
99
+ end
100
+
101
+ if collection.kind_of?(Array)
102
+ if !collection[0].kind_of?(Array)
103
+ collection.map! { |option| [option, option] }
104
+ end
105
+ elsif collection.kind_of?(Hash)
106
+ collection = collection.to_a
107
+ end
108
+
109
+ if blank_option
110
+ collection.insert(0,["null", t("views.select_boxes.no_value")])
111
+ end
112
+
113
+ collection.map! { |option| [option[0].to_s, option[1].to_s] }
114
+ collection
115
+
116
+ end
117
+
118
+ def select_component_get_selected_value(options, type=:input)
119
+
120
+ input_value = if options[:selected]
121
+ options[:selected]
122
+ elsif options[:name]
123
+ get_value_for_field_name(options)
124
+ end
125
+
126
+ if type == :input
127
+ input_value
128
+ else
129
+ if options[:collection].blank? && !get_value_for_field_name(options)
130
+ return get_value_for_field_name(options, from_params: true)
131
+ end
132
+ prepare_select_collection(options[:collection]).to_h[input_value]
133
+ end
134
+
135
+ end
136
+
137
+ def get_value_for_field_name(options, from_params: false)
138
+
139
+ @model_array_fields_counter ||= {}
140
+
141
+ if options[:name] && options[:name].include?("[")
142
+ names = get_field_names_chain_from_nested_field_param_and_model(options)
143
+ model_name = names.shift
144
+
145
+ if options[:model] && !from_params
146
+ field_name = options[:model_field_name] || options[:attr_name]
147
+ if field_name.blank?
148
+ return nil
149
+ else
150
+ if options[:nested_field]
151
+ model = get_target_model(names, options)
152
+ model.send(field_name) if model
153
+ else
154
+ field = options[:model].send(names.shift)
155
+ names.each do |n|
156
+ return nil if n.nil? || field.nil?
157
+ # This deals with fields that are arrays, for example quiz_question[answers][en][]
158
+ # In case field name ends with [] (empty string in `n`), we start counting how many fields
159
+ # with such a name exist and pull values from the model's array stored in that field.
160
+ if n.to_s == ""
161
+ @model_array_fields_counter[options[:name]] ||= 0
162
+ field = field[@model_array_fields_counter[options[:name]]]
163
+ @model_array_fields_counter[options[:name]] = @model_array_fields_counter[options[:name]] + 1
164
+ else
165
+ field = field[n]
166
+ end
167
+ end
168
+ return field
169
+ end
170
+ end
171
+ else
172
+ field_name = options[:attr_name]
173
+ value = params[options[:model_name]]
174
+ names.each do |fn|
175
+ value = if fn.kind_of?(Array) && value
176
+ m = value["#{fn[0]}_attributes"]
177
+ m[options[:field_counter]] if m
178
+ else
179
+ value["#{fn}_attributes"] if value
180
+ end
181
+ end
182
+ return value[field_name] if value
183
+ end
184
+ else
185
+ params[options[:name]]
186
+ end
187
+ end
188
+
189
+ def get_error_for_field_name(options)
190
+ names = get_field_names_chain_from_nested_field_param_and_model(options)
191
+ model_name = names.shift
192
+ field_name = options[:attr_name]
193
+ return unless field_name
194
+
195
+ if !options[:model].blank? && model = get_target_model(names, options)
196
+ model.errors[field_name]
197
+ end
198
+ end
199
+
200
+ def get_field_names_chain_from_nested_field_param_and_model(options)
201
+ result = [options[:model].class.to_s.underscore]
202
+
203
+ # associated model
204
+ if assoc = options[:nested_field]
205
+ assoc.each do |a|
206
+ if a[1] == :has_one
207
+ result << a[0]
208
+ else
209
+ result << [a[0], a[2]]
210
+ end
211
+ end
212
+
213
+ # serialized hash
214
+ elsif options[:name]
215
+ result = []
216
+ options[:name].split("[").map { |n| n.chomp("]") }.each do |n|
217
+ n = if n =~ /\A\d+\Z/
218
+ n.to_i
219
+ else
220
+ n.to_sym
221
+ end
222
+ result << n
223
+ end
224
+ end
225
+ result
226
+ end
227
+
228
+ def get_target_model(names, options)
229
+ model = options[:model]
230
+ return model unless options[:nested_field]
231
+
232
+ names.each_with_index do |fn,i|
233
+ return nil if model.nil?
234
+ model = if fn.kind_of?(Array)
235
+ collection = model.send(fn[0])
236
+ if fn[1]
237
+ collection.select { |m| fn[1] == m.id }.first
238
+ else
239
+ collection[options[:field_counter]]
240
+ end
241
+ else
242
+ model.send(fn)
243
+ end
244
+ end
245
+ model
246
+ end
247
+
248
+ end
@@ -0,0 +1,161 @@
1
+ class WebfaceForm
2
+
3
+ OPTION_ATTRS = [:tabindex, :label, :suffix, :selected, :hint, :collection, :allow_custom_value, :fetch_url, :query_param_name, :disabled, :nested_field, :serialized_hash, :model_field_name, :model_name, :radio_options_id, :popup_hint, :has_blank]
4
+
5
+ class NoSuchFormFieldType < Exception;end
6
+
7
+ def initialize(model, view_context)
8
+ @model = model
9
+ @view_context = view_context
10
+ @field_counter = {}
11
+ end
12
+
13
+ def method_missing(m, *args)
14
+
15
+ attrs = {}
16
+ options = {}
17
+ if args.last.kind_of?(Hash)
18
+ attrs = args.last
19
+ options = separate_option_attrs(attrs)
20
+ end
21
+ field_type = m
22
+
23
+ unless self.class.private_method_defined?("_#{field_type}")
24
+ raise NoSuchFormFieldType, "Field type `#{field_type}` not supported"
25
+ end
26
+
27
+ field_name = if args.first.blank? || args.first.to_s =~ /!\Z/
28
+ args.first.to_s.sub("!", "")
29
+ else
30
+ "#{@model.class.to_s.underscore}[#{args.first}]"
31
+ end
32
+
33
+ if options[:nested_field]
34
+ field_name = modify_name_for_nested_field(field_name, options[:nested_field])
35
+ elsif args.first && args.first.to_s.include?("[") && options[:nested_field].nil?
36
+ field_name = modify_name_for_serialized_field(field_name)
37
+ end
38
+
39
+ if @field_counter[field_name]
40
+ @field_counter[field_name] += 1
41
+ else
42
+ @field_counter[field_name] = 0
43
+ end
44
+ options[:field_counter] = @field_counter[field_name]
45
+
46
+ options[:radio_options_id] = options[:name] if options[:name]
47
+
48
+ options.merge!({ name: field_name, attr_name: args.first, model: @model, model_name: self.model_name})
49
+ attrs.delete_if { |k,v| OPTION_ATTRS.include?(k) }
50
+ attrs.merge!({ options: options })
51
+
52
+ if attrs[:options][:label].nil?
53
+ attrs[:options][:label] = I18n.t("models.#{field_name_for_i18n(field_name)}")
54
+ end
55
+
56
+ self.send("_#{field_type}", attrs)
57
+ end
58
+
59
+ def model_name
60
+ if @model
61
+ @model.class.to_s.underscore
62
+ else
63
+ options[:name].split("[").first
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def _text(attrs)
70
+ @view_context.component :text_form_field, attrs
71
+ end
72
+
73
+ def _password(attrs)
74
+ @view_context.component :password_form_field, attrs
75
+ end
76
+
77
+ def _hidden(attrs)
78
+ @view_context.component :hidden_form_field, attrs
79
+ end
80
+
81
+ def _numeric(attrs)
82
+ @view_context.component :numeric_form_field, attrs
83
+ end
84
+
85
+ def _textarea(attrs)
86
+ @view_context.component :textarea_form_field, attrs
87
+ end
88
+
89
+ def _checkbox(attrs)
90
+ @view_context.component :checkbox, attrs
91
+ end
92
+
93
+ def _select(attrs)
94
+ @view_context.component :select, attrs
95
+ end
96
+
97
+ def _editable_select(attrs)
98
+ @view_context.component :editable_select, attrs
99
+ end
100
+
101
+ def _radio(attrs)
102
+ @view_context.component :radio, attrs
103
+ end
104
+
105
+ def _submit(attrs)
106
+ attrs[:data] ||= {}
107
+ attrs[:data][:prevent_native_click_event] = "false"
108
+ @view_context.component :button, attrs.merge({options: { type: :submit}})
109
+ end
110
+
111
+ def separate_option_attrs(attrs)
112
+ option_attrs = {}
113
+ if attrs.kind_of?(Hash)
114
+ OPTION_ATTRS.each do |o|
115
+ option_attrs[o] = attrs[o] if !attrs[o].nil?
116
+ end
117
+ end
118
+ option_attrs
119
+ end
120
+
121
+ def modify_name_for_nested_field(name, association_name)
122
+ if association_name.kind_of?(Array)
123
+ association_name.each do |a|
124
+ name = send("modify_name_for_nested_#{a[1]}", name, a[0])
125
+ end
126
+ return name
127
+ else
128
+ modify_name_for_nested_has_many(name, association_name)
129
+ end
130
+ end
131
+
132
+ def modify_name_for_serialized_field(field_name)
133
+ model,remainder = field_name.split("[",2)
134
+ remainder.chomp!("]")
135
+ field_name,remainder = remainder.split("[",2)
136
+ "#{model}[#{field_name}][#{remainder}"
137
+ end
138
+
139
+ def modify_name_for_nested_has_one(name, nested_has_one_name)
140
+ # name == model[attribute]
141
+ # result == model[association][attribute]
142
+ model_name = name.scan(/\A.*?\[/)[0].sub("[", "")
143
+ field_names = name.scan(/\[.*?\]/)
144
+ actual_field_name = field_names.pop
145
+ "#{model_name}#{field_names.join("")}[#{nested_has_one_name}_attributes]#{actual_field_name}"
146
+ end
147
+
148
+ def modify_name_for_nested_has_many(name, nested_has_many_name)
149
+ # name == model[attribute]
150
+ # result == model[association][][attribute]
151
+ model_name = name.scan(/\A.*?\[/)[0].sub("[", "")
152
+ field_names = name.scan(/\[.*?\]/)
153
+ actual_field_name = field_names.pop
154
+ "#{model_name}#{field_names.join("")}[#{nested_has_many_name}_attributes][]#{actual_field_name}"
155
+ end
156
+
157
+ def field_name_for_i18n(fn)
158
+ fn = fn.split("[").map { |n| n.sub("]", "") }.join(".")
159
+ end
160
+
161
+ end