├── .gitignore
├── .rspec
├── .travis.yml
├── .yardopts
├── CHANGELOG.md
├── Gemfile
├── LICENSE.md
├── README.md
├── Rakefile
├── bin
└── console
├── formalist.gemspec
├── lib
├── formalist.rb
└── formalist
│ ├── child_forms
│ ├── builder.rb
│ ├── child_form.rb
│ ├── params_processor.rb
│ └── validity_check.rb
│ ├── definition.rb
│ ├── element.rb
│ ├── element
│ ├── attributes.rb
│ └── class_interface.rb
│ ├── elements.rb
│ ├── elements
│ ├── attr.rb
│ ├── compound_field.rb
│ ├── field.rb
│ ├── form_field.rb
│ ├── group.rb
│ ├── many.rb
│ ├── many_forms.rb
│ ├── section.rb
│ ├── standard.rb
│ └── standard
│ │ ├── check_box.rb
│ │ ├── date_field.rb
│ │ ├── date_time_field.rb
│ │ ├── hidden_field.rb
│ │ ├── multi_selection_field.rb
│ │ ├── multi_upload_field.rb
│ │ ├── number_field.rb
│ │ ├── radio_buttons.rb
│ │ ├── rich_text_area.rb
│ │ ├── search_multi_selection_field.rb
│ │ ├── search_selection_field.rb
│ │ ├── select_box.rb
│ │ ├── selection_field.rb
│ │ ├── tags_field.rb
│ │ ├── text_area.rb
│ │ ├── text_field.rb
│ │ └── upload_field.rb
│ ├── form.rb
│ ├── form
│ └── validity_check.rb
│ ├── rich_text
│ ├── embedded_form_compiler.rb
│ ├── embedded_forms_container.rb
│ ├── embedded_forms_container
│ │ ├── mixin.rb
│ │ └── registration.rb
│ ├── rendering
│ │ ├── embedded_form_renderer.rb
│ │ ├── html_compiler.rb
│ │ └── html_renderer.rb
│ └── validity_check.rb
│ └── version.rb
└── spec
├── integration
├── dependency_injection_spec.rb
└── form_spec.rb
├── spec_helper.rb
├── support
└── constants.rb
└── unit
├── child_forms
├── builder_spec.rb
├── child_form_spec.rb
├── params_processor_spec.rb
└── validity_check_spec.rb
├── elements
├── many_forms_spec.rb
└── standard
│ └── check_box_spec.rb
├── form
└── validity_check_spec.rb
└── rich_text
├── embedded_form_compiler_spec.rb
├── rendering
├── embedded_form_renderer_spec.rb
└── html_compiler_spec.rb
└── validity_check_spec.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.byebug_history
2 | /.yardoc
3 | /doc
4 | /coverage
5 | /spec/examples.txt
6 | /pkg
7 | /Gemfile.lock
8 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | dist: trusty
3 | sudo: false
4 | cache: bundler
5 | before_install:
6 | - gem update bundler
7 | script:
8 | - bundle exec rake
9 | after_success:
10 | # Send coverage report from the job #1 == current MRI release
11 | - '[ "${TRAVIS_JOB_NUMBER#*.}" = "1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter'
12 | rvm:
13 | - 2.4.0
14 | - 2.3.3
15 | - 2.2.6
16 | - jruby-9.1.6.0
17 | addons:
18 | code_climate:
19 | repo_token: 2c2c7c253435c02667371778ca886b069d4d24590748fbd7396b9b080016bfa7
20 | notifications:
21 | email: false
22 |
--------------------------------------------------------------------------------
/.yardopts:
--------------------------------------------------------------------------------
1 | --markup=markdown
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.9.0 / 2022-06-30
2 |
3 | ### Added
4 |
5 | - Added a `#with` method to `EmbeddedFormRenderer` to support additional call time options
6 |
7 | # 0.8.0 / 2022-06-29
8 |
9 | ### Changed
10 |
11 | - Update to ruby to version 3
12 | - Update dry-configurable to version 0.13
13 |
14 | # 0.7.0 / 2022-04-07
15 |
16 | ### Added
17 |
18 | - Add support a field with an arbitrary list of forms. Supporting updates in formalist-standard-react@^4.2.0
19 | - Add namespace and paths options to embedded form renderer
20 | - Support dry-schema/dry-validation 1.0
21 |
22 | # 0.6.0 / 2020-05-06
23 |
24 | ### Changed
25 |
26 | - Removed dry-types dependency from core gem
27 |
28 | # 0.5.4 / 2018-11-28
29 |
30 | ### Fixed
31 |
32 | - Fixed the rendering of validation errors when validating a `Many` element itself
33 |
34 | # 0.5.3 / 2018-09-25
35 |
36 | ### Added
37 |
38 | - Added `initial_attributes_url` attribute to `UploadField` and `MultiUploadField`
39 |
40 | # 0.5.2 / 2018-08-06
41 |
42 | ### Added
43 |
44 | - Added `clear_query_on_selection` attribute to `SearchMultiSelectionField`
45 |
46 | # 0.5.1 / 2018-07-19
47 |
48 | ### Added
49 |
50 | - Add `disabled` attribute to `TextField`
51 | - Add `time_format` and `human_time_format` attributes to `DateTimeField`
52 | - Add `sortable` and `max_height` attributes to `Many`, `MultiSelectionField`, `MultiUploadField`, and `SearchMultiSelectionField`
53 |
54 | ### Fixed
55 |
56 | - Allow falsey attribute values to be passed through in AST
57 |
58 | # 0.5.0 / 2018-07-04
59 |
60 | ### Added
61 |
62 | - Add `render_option_control_as` option to search select fields
63 |
64 | ### Changed
65 |
66 | - dry-types dependency updated to 0.13
67 |
68 | # 0.4.2 / 2018-07-03
69 |
70 | ### Fixed
71 |
72 | - Errors passed to `#fill` are now properly stored on the form's elements
73 |
74 | ### Changed
75 |
76 | - Private form methods are accessible from within definition blocks
77 |
78 | # 0.4.1 / 2018-04-17
79 |
80 | ### Fixed
81 |
82 | - Fixed issue with Form::Validity check crashing while processing `many` elements
83 |
84 | # 0.4.0 / 2018-03-28
85 |
86 | ### Added
87 |
88 | - `rich_text_area` field type, support for embedding and validating forms within rich text areas, and rendering the rich text from its native draft.js AST format to HTML
89 | - `search_selection_field` and `multi_search_selection_field` field types
90 | - Various improvements to the upload fields, including support for passing `presign_options`
91 |
92 | ### Changed
93 |
94 | - [BREAKING] Form definition blocks are now evaluated within the context of the form instance's `self`, which mean dependencies can be injected into the form object and accessed from the form definition block. `#dep` within the definition block has thusly been removed.
95 | - [BREAKING] `Formalist::Form` should now be instantiated (`form = MyForm.new`) and then filled with `form.fill(input: input_data, errors: error_messages)`. `Form#fill` returns a copy of the form object with all the elements filled with the input data and error messages as required. `Form#call` has been removed, along with `Formalist::Form::Result` (which was the object returned from `#call`).
96 |
97 | ### Removed
98 |
99 | - Form elements no longer have a `permitted_children` config. For now, this should be handled by convention only.
100 |
101 | # 0.3.0 / 2016-05-04
102 |
103 | Add support for upload and multi upload fields.
104 |
105 | # 0.2.3 / 2016-04-07
106 |
107 | Default check_box element values to false.
108 |
109 | # 0.2.2 / 2016-02-23
110 |
111 | Remove local type coercion using dry-data. We rely on a `Dry::Validation::Schema` to do this now. The form definition API has not yet changed, though. We still require field types to be specified, but there is no longer any restriction over what is entered. We'll remove this in a future release, once we can infer types from the schema.
112 |
113 | # 0.2.1 / 2016-02-23
114 |
115 | Fix issue where form could not be built with input data with native data types (it presuming input would be HTML form-style input everywhere).
116 |
117 | Add default (empty) input data argument for `Form#build`. This allows you simply to call `MyForm.build` for creating a "new" form (i.e. one without any existing input data).
118 |
119 | # 0.2.0 / 2016-02-22
120 |
121 | Require a dry-validation schema for each form. The schema should completely represent the form's expected data structure.
122 |
123 | Update the API to better support the two main use cases for a form. First, to prepare a form with sane, initial input from your database and send it to a view for rendering:
124 |
125 | ```ruby
126 | my_form = MyForm.new(my_schema)
127 | my_form.build(input) # returns a `Formalist::Form::Result`
128 | ```
129 |
130 | Then, to receive the data from the posted form, coerce it and validate it (and potentially re-display the form with errors):
131 |
132 | ```ruby
133 | my_form = MyForm.new(my_schema)
134 | my_form.receive(input).validate # returns a `Formalist::Form::ValidatedResult`
135 | ```
136 |
137 | The main differences are as such:
138 |
139 | * `#build` expects already-clean (e.g. properly structured and typed) data
140 | * `#receive` expects the kind of data that your form will submit, and will then coerce it according to the validation schema, and then send the sanitised output to `#build`
141 | * Calling `#validate` on a result object will validate the input data and include any error messages in its AST
142 |
143 | # 0.1.0 / 2016-01-18
144 |
145 | Initial release.
146 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gemspec
4 |
5 | group :test do
6 | gem "codeclimate-test-reporter", require: nil
7 | gem "dry-auto_inject"
8 | gem "dry-validation", "~> 1.0"
9 | end
10 |
11 | group :tools do
12 | gem "pry"
13 | gem "byebug", platform: :mri
14 | end
15 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2015-2016 [Icelab](http://icelab.com.au/).
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [gem]: https://rubygems.org/gems/formalist
2 | [travis]: https://travis-ci.org/icelab/formalist
3 | [code_climate]: https://codeclimate.com/github/icelab/formalist
4 | [inch]: http://inch-ci.org/github/icelab/formalist
5 |
6 | # Formalist
7 |
8 | [][gem]
9 | [][travis]
10 | [][code_climate]
11 | [][code_climate]
12 | [][inch]
13 |
14 | ## Installation
15 |
16 | Add this line to your application’s `Gemfile`:
17 |
18 | ```ruby
19 | gem "formalist"
20 | ```
21 |
22 | Run `bundle` to install the gems.
23 |
24 | ## Contributing
25 |
26 | Bug reports and pull requests are welcome on [GitHub](http://github.com/icelab/formalist).
27 |
28 | ## Credits
29 |
30 | Formalist is developed and maintained by [Icelab](http://icelab.com.au/).
31 |
32 | ## License
33 |
34 | Copyright © 2015-2016 [Icelab](http://icelab.com.au/). Formalist is free software, and may be redistributed under the terms specified in the [license](LICENSE.md).
35 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
3 | require "rspec/core/rake_task"
4 | RSpec::Core::RakeTask.new
5 |
6 | task default: :spec
7 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 |
5 | require "pry"
6 | begin
7 | require "byebug"
8 | rescue LoadError; end
9 | require "formalist"
10 | require "formalist/elements/standard"
11 | require "dry-validation"
12 |
13 | Pry.start
14 |
--------------------------------------------------------------------------------
/formalist.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path("../lib", __FILE__)
2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3 | require "formalist/version"
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = "formalist"
7 | spec.version = Formalist::VERSION
8 | spec.authors = ["Tim Riley"]
9 | spec.email = ["tim@icelab.com.au"]
10 | spec.license = "MIT"
11 |
12 | spec.summary = "Flexible form builder"
13 | spec.homepage = "https://github.com/icelab/formalist"
14 |
15 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
16 | spec.bindir = 'exe'
17 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18 | spec.require_paths = ['lib']
19 |
20 | spec.required_ruby_version = ">= 3.0.0"
21 |
22 | spec.add_runtime_dependency "dry-configurable", "~> 0.13"
23 | spec.add_runtime_dependency "dry-core", "~> 0.4"
24 | spec.add_runtime_dependency "dry-container", "~> 0.6"
25 | spec.add_runtime_dependency "inflecto"
26 |
27 | spec.add_development_dependency "bundler"
28 | spec.add_development_dependency "rake", "~> 10.4"
29 | spec.add_development_dependency "rspec", "~> 3.3.0"
30 | spec.add_development_dependency "simplecov", "~> 0.13.0"
31 | spec.add_development_dependency "yard"
32 | end
33 |
--------------------------------------------------------------------------------
/lib/formalist.rb:
--------------------------------------------------------------------------------
1 | require "formalist/form"
2 |
--------------------------------------------------------------------------------
/lib/formalist/child_forms/builder.rb:
--------------------------------------------------------------------------------
1 | require "json"
2 | require_relative "child_form"
3 |
4 | module Formalist
5 | module ChildForms
6 | class Builder
7 | attr_reader :embedded_forms
8 | MissingFormDefinitionError = Class.new(StandardError)
9 |
10 | def initialize(embedded_form_collection)
11 | @embedded_forms = embedded_form_collection
12 | end
13 |
14 | def call(input)
15 | return input if input.nil?
16 | input.map { |node| visit(node) }
17 | end
18 | alias_method :[], :call
19 |
20 | private
21 |
22 | def visit(node)
23 | name, data = node.values_at(:name, :data)
24 |
25 | embedded_form = embedded_forms[name]
26 | if embedded_form.nil?
27 | raise MissingFormDefinitionError, "Form +#{embedded_forms[name]}+ is missing from the embeddable forms collection"
28 | end
29 | child_form(name, embedded_form).fill(input: data)
30 | end
31 |
32 | def child_form(name, embedded_form)
33 | ChildForm.build(
34 | name: name,
35 | attributes: {
36 | label: embedded_form.label,
37 | form: embedded_form.form,
38 | schema: embedded_form.schema,
39 | input_processor: embedded_form.input_processor,
40 | preview_image_url: embedded_form.preview_image_url
41 | }
42 | )
43 | end
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/formalist/child_forms/child_form.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 |
3 | module Formalist
4 | module ChildForms
5 | class ChildForm < Element
6 | DEFAULT_INPUT_PROCESSOR = -> input { input }.freeze
7 |
8 | attribute :label
9 | attribute :form
10 | attribute :schema
11 | attribute :input_processor, default: DEFAULT_INPUT_PROCESSOR
12 | attribute :preview_image_url
13 |
14 | def fill(input: {}, errors: {})
15 | super(input: form_input_ast(input), errors: errors.to_a)
16 | end
17 |
18 | def attributes
19 | super.merge(form: form_attribute_ast)
20 | end
21 |
22 | def form_attribute_ast
23 | @attributes[:form].to_ast
24 | end
25 |
26 | def form_input_ast(data)
27 | # Run the raw data through the validation schema
28 | validation = @attributes[:schema].(data)
29 |
30 | # And then through the embedded form's input processor (which may add
31 | # extra system-generated information necessary for the form to render
32 | # fully)
33 | input = @attributes[:input_processor].(validation.to_h)
34 |
35 | @attributes[:form].fill(input: input, errors: validation.errors.to_h).to_ast
36 | end
37 |
38 | def to_ast
39 | [:child_form, [
40 | name,
41 | type,
42 | input,
43 | Element::Attributes.new(attributes).to_ast,
44 | ]]
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/formalist/child_forms/params_processor.rb:
--------------------------------------------------------------------------------
1 | require "json"
2 | require_relative "builder"
3 |
4 | module Formalist
5 | module ChildForms
6 | class ParamsProcessor
7 | attr_reader :embedded_forms
8 |
9 | def initialize(embedded_form_collection)
10 | @embedded_forms = embedded_form_collection
11 | end
12 |
13 | def call(input)
14 | return input if input.nil?
15 | input.inject([]) { |output, node| output.push(process(node)) }
16 | end
17 | alias_method :[], :call
18 |
19 | private
20 |
21 | def process(node)
22 | name, data = node.values_at(:name, :data)
23 |
24 | validation = embedded_forms[name].schema.(data)
25 | node.merge(data: validation.to_h)
26 | end
27 |
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/formalist/child_forms/validity_check.rb:
--------------------------------------------------------------------------------
1 | require "json"
2 | require_relative "builder"
3 |
4 | module Formalist
5 | module ChildForms
6 | class ValidityCheck
7 | attr_reader :embedded_forms
8 |
9 | def initialize(embedded_form_collection)
10 | @embedded_forms = embedded_form_collection
11 | end
12 |
13 | def call(input)
14 | return input if input.nil?
15 | input.map { |node| valid?(node) }.all?
16 | end
17 | alias_method :[], :call
18 |
19 | private
20 |
21 | def valid?(node)
22 | name, data = node.values_at(:name, :data)
23 |
24 | validation = embedded_forms[name].schema
25 | validation.(data).success?
26 | end
27 |
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/formalist/definition.rb:
--------------------------------------------------------------------------------
1 | module Formalist
2 | class Definition
3 | DuplicateElementError = Class.new(StandardError)
4 |
5 | attr_reader :form
6 | attr_reader :config
7 | attr_reader :elements
8 |
9 | def initialize(form, config, &block)
10 | @form = form
11 | @config = config
12 | @elements = []
13 |
14 | instance_eval(&block) if block
15 | end
16 |
17 | def with(**new_options, &block)
18 | new_config = new_options.each_with_object(config.dup) { |(key, value), config|
19 | config.send :"#{key}=", value
20 | }
21 |
22 | self.class.new(form, new_config, &block)
23 | end
24 |
25 | def method_missing(name, *args, &block)
26 | if element_type?(name)
27 | add_element(name, *args, &block)
28 | elsif form.respond_to?(name, include_private = true)
29 | form.send(name, *args, &block)
30 | else
31 | super
32 | end
33 | end
34 |
35 | private
36 |
37 | def respond_to_missing?(name, _include_private = false)
38 | element_type?(name) || form.respond_to?(name, _include_private = true) || super
39 | end
40 |
41 | def element_type?(type)
42 | config.elements_container.key?(type)
43 | end
44 |
45 | def add_element(type, *args, &block)
46 | element_name = args.shift unless args.first.is_a?(Hash)
47 | element_attrs = args.last.is_a?(Hash) ? args.last : {}
48 |
49 | if element_name && elements.any? { |element| element.name == element_name }
50 | raise DuplicateElementError, "element +#{element_name}+ is already defined in this context"
51 | end
52 |
53 | element_class = config.elements_container[type]
54 | element_children = with(&block).elements
55 |
56 | element = element_class.build(
57 | name: element_name,
58 | attributes: element_attrs,
59 | children: element_children,
60 | )
61 |
62 | elements << element
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/formalist/element.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element/attributes"
2 | require "formalist/element/class_interface"
3 |
4 | module Formalist
5 | class Element
6 | extend ClassInterface
7 |
8 | # @api private
9 | attr_reader :name, :attributes, :children, :input, :errors
10 |
11 | # @api private
12 | def self.build(**args)
13 | new(**args)
14 | end
15 |
16 | # @api private
17 | def self.fill(input: {}, errors: {}, **args)
18 | new(**args).fill(input: input, errors: errors)
19 | end
20 |
21 | # @api private
22 | def initialize(name: nil, attributes: {}, children: [], input: nil, errors: [])
23 | @name = name&.to_sym
24 |
25 | @attributes = self.class.attributes_schema.each_with_object({}) { |(name, defn), hsh|
26 | value = attributes.fetch(name) { defn[:default] }
27 | hsh[name] = value unless value.nil?
28 | }
29 |
30 | @children = children
31 | @input = input
32 | @errors = errors
33 | end
34 |
35 | def fill(input: {}, errors: {}, **args)
36 | return self if input == @input && errors == @errors
37 |
38 | args = {
39 | name: @name,
40 | attributes: @attributes,
41 | children: @children,
42 | input: input,
43 | errors: errors,
44 | }.merge(args)
45 |
46 | self.class.new(**args)
47 | end
48 |
49 | def type
50 | self.class.type
51 | end
52 |
53 | def ==(other)
54 | name && type == other.type && name == other.name
55 | end
56 |
57 | # @abstract
58 | def to_ast
59 | raise NotImplementedError
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/formalist/element/attributes.rb:
--------------------------------------------------------------------------------
1 | module Formalist
2 | class Element
3 | class Attributes
4 | # Returns the attributes hash.
5 | attr_reader :attrs
6 |
7 | # Creates an attributes object from the supplied hash.
8 | #
9 | # @param attrs [Hash] hash of form element attributes
10 | def initialize(attrs = {})
11 | @attrs = attrs
12 | end
13 |
14 | # Returns the attributes as an abstract syntax tree.
15 | #
16 | # @return [Array] the abstract syntax tree
17 | def to_ast
18 | deep_to_ast(deep_simplify(attrs))
19 | end
20 |
21 | private
22 |
23 | def deep_to_ast(value)
24 | case value
25 | when Hash
26 | [:object, [value.map { |k,v| [k.to_sym, deep_to_ast(v)] }].reject(&:empty?).flatten(1)]
27 | when Array
28 | [:array, value.map { |v| deep_to_ast(v) }]
29 | when String, Numeric, TrueClass, FalseClass, NilClass
30 | [:value, [value]]
31 | else
32 | [:value, [value.to_s]]
33 | end
34 | end
35 |
36 | def deep_simplify(value)
37 | case value
38 | when Hash
39 | value.each_with_object({}) { |(k,v), output| output[k] = deep_simplify(v) }
40 | when Array
41 | value.map { |v| deep_simplify(v) }
42 | when String, Numeric, TrueClass, FalseClass, NilClass
43 | value
44 | else
45 | if value.respond_to?(:to_h)
46 | deep_simplify(value.to_h)
47 | else
48 | value.to_s
49 | end
50 | end
51 | end
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/formalist/element/class_interface.rb:
--------------------------------------------------------------------------------
1 | require "inflecto"
2 |
3 | module Formalist
4 | class Element
5 | # Class-level API for form elements.
6 | module ClassInterface
7 | def type
8 | Inflecto.underscore(Inflecto.demodulize(name)).to_sym
9 | end
10 |
11 | def attribute(name, default: nil)
12 | attributes(name => {default: default})
13 | end
14 |
15 | def attributes_schema
16 | super_schema = superclass.respond_to?(:attributes_schema) ? superclass.attributes_schema : {}
17 | super_schema.merge(@attributes_schema || {})
18 | end
19 |
20 | private
21 |
22 | def attributes(new_schema)
23 | prev_schema = @attributes_schema || {}
24 | @attributes_schema = prev_schema.merge(new_schema)
25 |
26 | self
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/formalist/elements.rb:
--------------------------------------------------------------------------------
1 | require "dry-container"
2 | require "formalist/elements/attr"
3 | require "formalist/elements/compound_field"
4 | require "formalist/elements/field"
5 | require "formalist/elements/group"
6 | require "formalist/elements/many"
7 | require "formalist/elements/many_forms"
8 | require "formalist/elements/form_field"
9 | require "formalist/elements/section"
10 |
11 | module Formalist
12 | class Elements
13 | extend Dry::Container::Mixin
14 |
15 | register :attr, Attr
16 | register :compound_field, CompoundField
17 | register :field, Field
18 | register :group, Group
19 | register :many, Many
20 | register :many_forms, ManyForms
21 | register :form_field, FormField
22 | register :section, Section
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/formalist/elements/attr.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 |
3 | module Formalist
4 | class Elements
5 | class Attr < Element
6 | attribute :label
7 |
8 | def fill(input:, errors:)
9 | input = input[name] || {}
10 | errors = errors[name] || {}
11 |
12 | children = self.children.map { |child|
13 | child.fill(input: input, errors: errors)
14 | }
15 |
16 | super(input: input, errors: errors, children: children)
17 | end
18 |
19 | # Converts the attribute into an abstract syntax tree.
20 | #
21 | # It takes the following format:
22 | #
23 | # ```
24 | # [:attr, [params]]
25 | # ```
26 | #
27 | # With the following parameters:
28 | #
29 | # 1. Attribute name
30 | # 2. Custom element type (or `:attr` otherwise)
31 | # 3. Error messages
32 | # 4. Form element attributes
33 | # 5. Child form elements
34 | #
35 | # @see Formalist::Element::Attributes#to_ast "Form element attributes" structure
36 | #
37 | # @example "metadata" attr
38 | # attr.to_ast
39 | # # => [:attr, [
40 | # :metadata,
41 | # :attr,
42 | # ["metadata is missing"],
43 | # [:object, []],
44 | # [...child elements...]
45 | # ]]
46 | #
47 | # @return [Array] the attribute as an abstract syntax tree.
48 | def to_ast
49 | local_errors = errors.is_a?(Array) ? errors : []
50 |
51 | [:attr, [
52 | name,
53 | type,
54 | local_errors,
55 | Element::Attributes.new(attributes).to_ast,
56 | children.map(&:to_ast),
57 | ]]
58 | end
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/lib/formalist/elements/compound_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 |
3 | module Formalist
4 | class Elements
5 | class CompoundField < Element
6 | def fill(input: {}, errors: {})
7 | children = self.children.map { |child|
8 | child.fill(input: input, errors: errors)
9 | }
10 |
11 | super(input: input, errors: errors, children: children)
12 | end
13 |
14 | # Converts the compound field into an abstract syntax tree.
15 | #
16 | # It takes the following format:
17 | #
18 | # ```
19 | # [:compound_field, [params]]
20 | # ```
21 | #
22 | # With the following parameters:
23 | #
24 | # 1. Custom element type (or `:compound_field` otherwise)
25 | # 2. Form element attributes
26 | # 3. Child form elements
27 | #
28 | # @see Formalist::Element::Attributes#to_ast "Form element attributes" structure
29 | #
30 | # @example
31 | # compound_field.to_ast
32 | # # => [:compound_field, [
33 | # :content,
34 | # :compound_field,
35 | # [:object, []],
36 | # [...child elements...],
37 | # ]]
38 | #
39 | # @return [Array] the compound field as an abstract syntax tree.
40 | def to_ast
41 | [:compound_field, [
42 | type,
43 | Element::Attributes.new(attributes).to_ast,
44 | children.map(&:to_ast),
45 | ]]
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/formalist/elements/field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 |
3 | module Formalist
4 | class Elements
5 | class Field < Element
6 | attribute :label
7 | attribute :hint
8 | attribute :placeholder
9 | attribute :inline
10 | attribute :validation
11 |
12 | def fill(input: {}, errors: {})
13 | super(
14 | input: input[name],
15 | errors: errors[name].to_a,
16 | )
17 | end
18 |
19 | # Converts the field into an abstract syntax tree.
20 | #
21 | # It takes the following format:
22 | #
23 | # ```
24 | # [:field, [params]]
25 | # ```
26 | #
27 | # With the following parameters:
28 | #
29 | # 1. Field name
30 | # 2. Custom form element type (or `:field` otherwise)
31 | # 3. Associated form input data
32 | # 4. Error messages
33 | # 5. Form element attributes
34 | #
35 | # @see Formalist::Element::Attributes#to_ast "Form element attributes" structure
36 | #
37 | # @example "email" field
38 | # field.to_ast
39 | # # => [:field, [
40 | # :email,
41 | # :field,
42 | # "jane@doe.org",
43 | # [],
44 | # [:object, []],
45 | # ]]
46 | #
47 | # @return [Array] the field as an abstract syntax tree.
48 | def to_ast
49 | # errors looks like this
50 | # {:field_name => [["pages is missing", "another error message"], nil]}
51 |
52 | [:field, [
53 | name,
54 | type,
55 | input,
56 | errors,
57 | Element::Attributes.new(attributes).to_ast,
58 | ]]
59 | end
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/formalist/elements/form_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/child_forms/child_form"
2 |
3 | module Formalist
4 | class Elements
5 | class FormField < ChildForms::ChildForm
6 | attribute :hint
7 |
8 | def fill(input: {}, errors: {})
9 | input = input[name]
10 | errors = errors[name].to_a
11 |
12 | super(input: input, errors: errors)
13 | end
14 |
15 | def to_ast
16 | [:form_field, [
17 | name,
18 | type,
19 | input,
20 | Element::Attributes.new(attributes).to_ast,
21 | ]]
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/formalist/elements/group.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 |
3 | module Formalist
4 | class Elements
5 | class Group < Element
6 | attribute :label
7 |
8 | def fill(input: {}, errors: {})
9 | children = self.children.map { |child|
10 | child.fill(input: input, errors: errors)
11 | }
12 |
13 | super(input: input, errors: errors, children: children)
14 | end
15 |
16 | # Converts the group into an abstract syntax tree.
17 | #
18 | # It takes the following format:
19 | #
20 | # ```
21 | # [:group, [params]]
22 | # ```
23 | #
24 | # With the following parameters:
25 | #
26 | # 1. Custom form element type (or `:group` otherwise)
27 | # 2. Form element attributes
28 | # 3. Child form elements
29 | #
30 | # @see Formalist::Element::Attributes#to_ast "Form element attributes" structure
31 | #
32 | # @example
33 | # group.to_ast
34 | # # => [:group, [
35 | # :group,
36 | # [:object, []],
37 | # [...child elements...]
38 | # ]]
39 | #
40 | # @return [Array] the group as an abstract syntax tree.
41 | def to_ast
42 | [:group, [
43 | type,
44 | Element::Attributes.new(attributes).to_ast,
45 | children.map(&:to_ast),
46 | ]]
47 | end
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/formalist/elements/many.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 |
3 | module Formalist
4 | class Elements
5 | class Many < Element
6 | attribute :action_label
7 | attribute :sortable
8 | attribute :moveable
9 | attribute :label
10 | attribute :max_height
11 | attribute :placeholder
12 | attribute :validation
13 | attribute :allow_create, default: true
14 | attribute :allow_update, default: true
15 | attribute :allow_destroy, default: true
16 | attribute :allow_reorder, default: true
17 |
18 | # @api private
19 | attr_reader :child_template
20 |
21 | # @api private
22 | def self.build(children: [], **args)
23 | child_template = children.dup
24 | super(child_template: child_template, **args)
25 | end
26 |
27 | # @api private
28 | def initialize(child_template:, **args)
29 | @child_template = child_template
30 | super(**args)
31 | end
32 |
33 | # @api private
34 | def fill(input: {}, errors: {})
35 | input = input.fetch(name) { [] }
36 | errors = errors[name] || {}
37 |
38 | # Errors look like this when they are on the array itself: ["size cannot be greater than 2"]
39 | # Errors look like this when they are on children: {0=>{:summary=>["must be filled"]}
40 |
41 | children = input.each_with_index.map { |child_input, index|
42 | child_errors = errors.is_a?(Hash) ? errors.fetch(index) { {} } : {}
43 |
44 | child_template.map { |child| child.fill(input: child_input, errors: child_errors) }
45 | }
46 |
47 | super(
48 | input: input,
49 | errors: errors,
50 | children: children,
51 | child_template: child_template,
52 | )
53 | end
54 |
55 | # Converts a collection of "many" repeating elements into an abstract
56 | # syntax tree.
57 | #
58 | # It takes the following format:
59 | #
60 | # ```
61 | # [:many, [params]]
62 | # ```
63 | #
64 | # With the following parameters:
65 | #
66 | # 1. Collection name
67 | # 2. Custom form element type (or `:many` otherwise)
68 | # 3. Collection-level error messages
69 | # 4. Form element attributes
70 | # 5. Child element "template" (i.e. the form elements comprising a
71 | # single entry in the collection of "many" elements, without any user
72 | # data associated)
73 | # 6. Child elements, one for each of the entries in the input data (or
74 | # none, if there is no or empty input data)
75 | #
76 | # @see Formalist::Element::Attributes#to_ast "Form element attributes" structure
77 | #
78 | # @example "locations" collection
79 | # many.to_ast
80 | # # => [:many, [
81 | # :locations,
82 | # :many,
83 | # ["locations size cannot be less than 3"],
84 | # [:object, [
85 | # [:allow_create, [:value, [true]]],
86 | # [:allow_update, [:value, [true]]],
87 | # [:allow_destroy, [:value, [true]]],
88 | # [:allow_reorder, [:value, [true]]]
89 | # ]],
90 | # [
91 | # [:field, [:name, :field, nil, [], [], [:object, []]]],
92 | # [:field, [:address, :field, nil, [], [], [:object, []]]]
93 | # ],
94 | # [
95 | # [
96 | # [:field, [:name, :field, "Icelab Canberra", [], [], [:object, []]]],
97 | # [:field, [:address, :field, "Canberra, ACT, Australia", [], [], [:object, []]]]
98 | # ],
99 | # [
100 | # [:field, [:name, :field, "Icelab Melbourne", [], [], [:object, []]]],
101 | # [:field, [:address, :field, "Melbourne, VIC, Australia", [], [], [:object, []]]]
102 | # ]
103 | # ]
104 | # ]]
105 | #
106 | # @return [Array] the collection as an abstract syntax tree.
107 | def to_ast
108 | local_errors = errors.is_a?(Array) ? errors : []
109 |
110 | [:many, [
111 | name,
112 | type,
113 | local_errors,
114 | Element::Attributes.new(attributes).to_ast,
115 | child_template.map(&:to_ast),
116 | children.map { |el_list| el_list.map(&:to_ast) },
117 | ]]
118 | end
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/lib/formalist/elements/many_forms.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/child_forms/builder"
3 |
4 | module Formalist
5 | class Elements
6 | class ManyForms < Element
7 | attribute :action_label
8 | attribute :sortable
9 | attribute :moveable
10 | attribute :label
11 | attribute :max_height
12 | attribute :placeholder
13 | attribute :embeddable_forms
14 | attribute :validation
15 | attribute :allow_create, default: true
16 | attribute :allow_update, default: true
17 | attribute :allow_destroy, default: true
18 | attribute :allow_reorder, default: true
19 |
20 | # FIXME: it would be tidier to have a reader method for each attribute
21 | def attributes
22 | super.merge(embeddable_forms: embeddable_forms_ast)
23 | end
24 |
25 | # @api private
26 | def fill(input: {}, errors: {})
27 | input = input[name] || []
28 | errors = errors[name].to_a
29 |
30 | children = child_form_builder.(input)
31 |
32 | super(
33 | input: input,
34 | errors: errors,
35 | children: children,
36 | )
37 | end
38 |
39 | # Replace the form objects with their AST
40 | def embeddable_forms_ast
41 | @attributes[:embeddable_forms].to_h.map { |key, attrs|
42 | template_attrs = attrs.slice(:label, :preview_image_url)
43 |
44 | [
45 | key,
46 | attrs.merge(
47 | form: attrs[:form].to_ast,
48 | attributes_template: Element::Attributes.new(template_attrs).to_ast
49 | )
50 | ]
51 | }.to_h
52 | end
53 |
54 | def child_form_builder
55 | ChildForms::Builder.new(@attributes[:embeddable_forms])
56 | end
57 |
58 | # Converts a collection of "many" repeating elements into an abstract
59 | # syntax tree.
60 | #
61 | # It takes the following format:
62 | #
63 | # ```
64 | # [:many_forms, [params]]
65 | # ```
66 | #
67 | # With the following parameters:
68 | #
69 | # 1. Collection name
70 | # 2. Custom form element type (or `:many_forms` otherwise)
71 | # 3. Collection-level error messages
72 | # 4. Form element attributes
73 | # 6. Child elements, one for each of the entries in the input data (or
74 | # none, if there is no or empty input data)
75 | #
76 | # @see Formalist::Element::Attributes#to_ast "Form element attributes" structure
77 | #
78 | # @example "components" collection
79 | # many_forms.to_ast
80 | # # => [:many_forms, [
81 | # :components,
82 | # :many_forms,
83 | # ["components size cannot be less than 3"],
84 | # [:object, [
85 | # [:allow_create, [:value, [true]]],
86 | # [:allow_update, [:value, [true]]],
87 | # [:allow_destroy, [:value, [true]]],
88 | # [:allow_reorder, [:value, [true]]]
89 | # ]],
90 | # [
91 | # [
92 | # [:child_form,
93 | # [:image_with_captions,
94 | # :child_form,
95 | # [[:field, [:image_id, :text_field, "", ["must be filled"], [:object, []]]], [:field, [:caption, :text_field, "Large panda", [], [:object, []]]]],
96 | # [:object, []]
97 | # ]
98 | # ]]
99 | #
100 | # @return [Array] the collection as an abstract syntax tree.
101 | def to_ast
102 | local_errors = errors.is_a?(Array) ? errors : []
103 |
104 | [:many_forms, [
105 | name,
106 | type,
107 | local_errors,
108 | Element::Attributes.new(attributes).to_ast,
109 | children.map(&:to_ast)
110 | ]]
111 | end
112 |
113 | end
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/lib/formalist/elements/section.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 |
3 | module Formalist
4 | class Elements
5 | class Section < Element
6 | attribute :label
7 |
8 | def fill(input: {}, errors: {})
9 | super(
10 | input: input,
11 | errors: errors,
12 | children: children.map { |child| child.fill(input: input, errors: errors) },
13 | )
14 | end
15 |
16 | # Converts the section into an abstract syntax tree.
17 | #
18 | # It takes the following format:
19 | #
20 | # ```
21 | # [:section, [params]]
22 | # ```
23 | #
24 | # With the following parameters:
25 | #
26 | # 1. Section name
27 | # 2. Custom form element type (or `:section` otherwise)
28 | # 3. Form element attributes
29 | # 4. Child form elements
30 | #
31 | # @see Formalist::Element::Attributes#to_ast "Form element attributes" structure
32 | #
33 | # @example "content" section
34 | # section.to_ast
35 | # # => [:section, [
36 | # :content,
37 | # :section,
38 | # [:object, []],
39 | # [...child elements...]
40 | # ]]
41 | #
42 | # @return [Array] the section as an abstract syntax tree.
43 | def to_ast
44 | [:section, [
45 | name,
46 | type,
47 | Element::Attributes.new(attributes).to_ast,
48 | children.map(&:to_ast),
49 | ]]
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard.rb:
--------------------------------------------------------------------------------
1 | require "formalist/elements/standard/check_box"
2 | require "formalist/elements/standard/date_field"
3 | require "formalist/elements/standard/date_time_field"
4 | require "formalist/elements/standard/hidden_field"
5 | require "formalist/elements/standard/multi_selection_field"
6 | require "formalist/elements/standard/multi_upload_field"
7 | require "formalist/elements/standard/number_field"
8 | require "formalist/elements/standard/radio_buttons"
9 | require "formalist/elements/standard/rich_text_area"
10 | require "formalist/elements/standard/search_selection_field"
11 | require "formalist/elements/standard/search_multi_selection_field"
12 | require "formalist/elements/standard/select_box"
13 | require "formalist/elements/standard/selection_field"
14 | require "formalist/elements/standard/tags_field"
15 | require "formalist/elements/standard/text_area"
16 | require "formalist/elements/standard/text_field"
17 | require "formalist/elements/standard/upload_field"
18 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/check_box.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class CheckBox < Field
7 | attribute :question_text
8 |
9 | def initialize(**)
10 | super
11 |
12 | # Ensure value is a boolean (also: default to false for nil values)
13 | @input = !!@input
14 | end
15 | end
16 |
17 | register :check_box, CheckBox
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/date_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class DateField < Field
7 | end
8 |
9 | register :date_field, DateField
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/date_time_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class DateTimeField < Field
7 | attribute :time_format
8 | attribute :human_time_format
9 | end
10 |
11 | register :date_time_field, DateTimeField
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/hidden_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class HiddenField < Field
7 | end
8 |
9 | register :hidden_field, HiddenField
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/multi_selection_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class MultiSelectionField < Field
7 | attribute :sortable
8 | attribute :max_height
9 | attribute :options
10 | attribute :render_option_as
11 | attribute :render_selection_as
12 | attribute :selector_label
13 | end
14 |
15 | register :multi_selection_field, MultiSelectionField
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/multi_upload_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class MultiUploadField < Field
7 | attribute :initial_attributes_url
8 | attribute :sortable
9 | attribute :max_file_size_message
10 | attribute :max_file_size
11 | attribute :max_height
12 | attribute :permitted_file_type_message
13 | attribute :permitted_file_type_regex
14 | attribute :presign_options
15 | attribute :presign_url
16 | attribute :render_uploaded_as
17 | attribute :upload_action_label
18 | attribute :upload_prompt
19 | end
20 |
21 | register :multi_upload_field, MultiUploadField
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/number_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class NumberField < Field
7 | attribute :step
8 | attribute :min
9 | attribute :max
10 | end
11 |
12 | register :number_field, NumberField
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/radio_buttons.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class RadioButtons < Field
7 | attribute :options
8 | end
9 |
10 | register :radio_buttons, RadioButtons
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/rich_text_area.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 | require "formalist/rich_text/embedded_form_compiler"
4 |
5 | module Formalist
6 | class Elements
7 | class RichTextArea < Field
8 | attribute :box_size, default: "normal"
9 | attribute :inline_formatters
10 | attribute :block_formatters
11 | attribute :embeddable_forms
12 |
13 | # FIXME: it would be tidier to have a reader method for each attribute
14 | def attributes
15 | super.merge(embeddable_forms: embeddable_forms_config)
16 | end
17 |
18 | def input
19 | input_compiler.(@input)
20 | end
21 |
22 | private
23 |
24 | # Replace the form objects with their AST
25 | def embeddable_forms_config
26 | @attributes[:embeddable_forms].to_h.map { |key, attrs|
27 | [key, attrs.merge(form: attrs[:form].to_ast)]
28 | }.to_h
29 | end
30 |
31 | # TODO: make compiler configurable somehow?
32 | def input_compiler
33 | RichText::EmbeddedFormCompiler.new(@attributes[:embeddable_forms])
34 | end
35 | end
36 |
37 | register :rich_text_area, RichTextArea
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/search_multi_selection_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class SearchMultiSelectionField < Field
7 | attribute :clear_query_on_selection
8 | attribute :sortable
9 | attribute :max_height
10 | attribute :render_option_as
11 | attribute :render_option_control_as
12 | attribute :render_selection_as
13 | attribute :search_params
14 | attribute :search_per_page
15 | attribute :search_threshold
16 | attribute :search_url
17 | attribute :selections
18 | attribute :selector_label
19 | end
20 |
21 | register :search_multi_selection_field, SearchMultiSelectionField
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/search_selection_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class SearchSelectionField < Field
7 | attribute :selector_label
8 | attribute :render_option_as
9 | attribute :render_option_control_as
10 | attribute :render_selection_as
11 | attribute :search_url
12 | attribute :search_per_page
13 | attribute :search_params
14 | attribute :search_threshold
15 | attribute :selection
16 | end
17 |
18 | register :search_selection_field, SearchSelectionField
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/select_box.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class SelectBox < Field
7 | attribute :options
8 | end
9 |
10 | register :select_box, SelectBox
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/selection_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class SelectionField < Field
7 | attribute :options
8 | attribute :selector_label
9 | attribute :render_option_as
10 | attribute :render_selection_as
11 | end
12 |
13 | register :selection_field, SelectionField
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/tags_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class TagsField < Field
7 | attribute :search_url
8 | attribute :search_per_page
9 | attribute :search_params
10 | attribute :search_threshold
11 | end
12 |
13 | register :tags_field, TagsField
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/text_area.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class TextArea < Field
7 | attribute :text_size, default: "normal"
8 | attribute :box_size, default: "normal"
9 | attribute :code
10 | end
11 |
12 | register :text_area, TextArea
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/text_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class TextField < Field
7 | attribute :password
8 | attribute :code
9 | attribute :disabled
10 | end
11 |
12 | register :text_field, TextField
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/formalist/elements/standard/upload_field.rb:
--------------------------------------------------------------------------------
1 | require "formalist/element"
2 | require "formalist/elements"
3 |
4 | module Formalist
5 | class Elements
6 | class UploadField < Field
7 | attribute :initial_attributes_url
8 | attribute :presign_url
9 | attribute :presign_options
10 | attribute :render_uploaded_as
11 | attribute :upload_prompt
12 | attribute :upload_action_label
13 | attribute :max_file_size
14 | attribute :max_file_size_message
15 | attribute :permitted_file_type_message
16 | attribute :permitted_file_type_regex
17 | end
18 |
19 | register :upload_field, UploadField
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/formalist/form.rb:
--------------------------------------------------------------------------------
1 | require "dry/configurable"
2 | require "dry/core/constants"
3 | require "formalist/elements"
4 | require "formalist/definition"
5 |
6 | module Formalist
7 | class Form
8 | extend Dry::Configurable
9 | include Dry::Core::Constants
10 |
11 | setting :elements_container, default: Elements
12 |
13 | class << self
14 | attr_reader :definition
15 |
16 | def define(&block)
17 | @definition = block
18 | end
19 | end
20 |
21 | attr_reader :elements
22 | attr_reader :input
23 | attr_reader :errors
24 | attr_reader :dependencies
25 |
26 | def initialize(elements: Undefined, input: {}, errors: {}, **dependencies)
27 | @input = input
28 | @errors = errors
29 |
30 | @elements =
31 | if elements == Undefined
32 | Definition.new(self, self.class.config, &self.class.definition).elements
33 | else
34 | elements
35 | end
36 |
37 | @dependencies = dependencies
38 | end
39 |
40 | def fill(input: {}, errors: {})
41 | return self if input == @input && errors == @errors
42 |
43 | self.class.new(
44 | elements: @elements.map { |element| element.fill(input: input, errors: errors) },
45 | input: input,
46 | errors: errors,
47 | **@dependencies,
48 | )
49 | end
50 |
51 | def with(**new_dependencies)
52 | self.class.new(
53 | elements: @elements,
54 | input: @input,
55 | errors: @errors,
56 | **@dependencies.merge(new_dependencies)
57 | )
58 | end
59 |
60 | def to_ast
61 | elements.map(&:to_ast)
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/lib/formalist/form/validity_check.rb:
--------------------------------------------------------------------------------
1 | module Formalist
2 | class Form
3 | class ValidityCheck
4 | def call(form_ast)
5 | form_ast.map { |node| visit(node) }.all?
6 | end
7 | alias_method :[], :call
8 |
9 | private
10 |
11 | def visit(node)
12 | name, nodes = node
13 |
14 | send(:"visit_#{name}", nodes)
15 | end
16 |
17 | def visit_attr(node)
18 | _name, _type, errors, _attributes, children = node
19 |
20 | errors.empty? && children.map { |child| visit(child) }.all?
21 | end
22 |
23 | def visit_compound_field(node)
24 | _type, _attributes, children = node
25 |
26 | children.map { |child| visit(child) }.all?
27 | end
28 |
29 | def visit_field(node)
30 | _name, _type, _input, errors, _attributes = node
31 |
32 | errors.empty?
33 | end
34 |
35 | def visit_group(node)
36 | _type, _attributes, children = node
37 |
38 | children.map { |child| visit(child) }.all?
39 | end
40 |
41 | def visit_many(node)
42 | _name, _type, errors, _attributes, _child_template, children = node
43 |
44 | # The `children parameter for `many` elements is nested since there are
45 | # many groups of elements, we need to flatten to traverse them all
46 | errors.empty? && children.flatten(1).map { |child| visit(child) }.all?
47 | end
48 |
49 | # TODO work out what to do with this.
50 | # I think it's only relevant to many_forms
51 | # nested in rich text ast
52 | def visit_many_forms(node)
53 | _name, _type, errors, _attributes, children = node
54 |
55 | errors.empty? && children.map { |child| visit(child[:form]) }.all?
56 | end
57 |
58 | def visit_section(node)
59 | _name, _type, _attributes, children = node
60 |
61 | children.map { |child| visit(child) }.all?
62 | end
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/formalist/rich_text/embedded_form_compiler.rb:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | module Formalist
4 | module RichText
5 |
6 | # Our input data looks like this example, which consists of 3 elements:
7 | #
8 | # 1. A text line
9 | # 2. embedded form data
10 | # 3. Another text line
11 | #
12 | # [
13 | # ["block",["unstyled","b14hd",[["inline",[[],"Before!"]]]]],
14 | # ["block",["atomic","48b4f",[["entity",["formalist","1","IMMUTABLE",{"name":"image_with_caption","label":"Image with caption","data":{"image_id":"5678","caption":"Large panda"}},[["inline",[[],"¶"]]]]]]]],
15 | # ["block",["unstyled","aivqi",[["inline",[[],"After!"]]]]]
16 | # ]
17 | #
18 | # We want to intercept the embededed form data and transform them into full
19 | # form ASTs, complete with validation messages.
20 |
21 | class EmbeddedFormCompiler
22 | attr_reader :embedded_forms
23 |
24 | def initialize(embedded_form_collection)
25 | @embedded_forms = embedded_form_collection
26 | end
27 |
28 | def call(ast)
29 | return ast if ast.nil?
30 |
31 | ast = ast.is_a?(String) ? JSON.parse(ast) : ast
32 |
33 | ast.map { |node| visit(node) }
34 | end
35 | alias_method :[], :call
36 |
37 | private
38 |
39 | def visit(node)
40 | name, nodes = node
41 |
42 | handler = :"visit_#{name}"
43 |
44 | if respond_to?(handler, true)
45 | send(handler, nodes)
46 | else
47 | [name, nodes]
48 | end
49 | end
50 |
51 | # We need to visit blocks in order to get to the formalist entities nested within them
52 | def visit_block(node)
53 | type, id, children = node
54 |
55 | ["block", [type, id, children.map { |child| visit(child) }]]
56 | end
57 |
58 | def visit_entity(node)
59 | type, key, mutability, entity_data, children = node
60 |
61 | return ["entity", node] unless type == "formalist"
62 |
63 | embedded_form = embedded_forms[entity_data["name"]]
64 |
65 | compiled_entity_data = entity_data.merge(
66 | "label" => embedded_form.label,
67 | "form" => prepare_form_ast(embedded_form, entity_data["data"])
68 | )
69 |
70 | ["entity", [type, key, mutability, compiled_entity_data, children]]
71 | end
72 |
73 | def prepare_form_ast(embedded_form, data)
74 | # Run the raw data through the validation schema
75 | validation = embedded_form.schema.(data)
76 |
77 | # And then through the embedded form's input processor (which may add
78 | # extra system-generated information necessary for the form to render
79 | # fully)
80 | input = embedded_form.input_processor.(validation.to_h)
81 |
82 | embedded_form.form.fill(input: input, errors: validation.errors.to_h).to_ast
83 | end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/lib/formalist/rich_text/embedded_forms_container.rb:
--------------------------------------------------------------------------------
1 | require "formalist/rich_text/embedded_forms_container/mixin"
2 |
3 | module Formalist
4 | module RichText
5 | class EmbeddedFormsContainer
6 | include Mixin
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/formalist/rich_text/embedded_forms_container/mixin.rb:
--------------------------------------------------------------------------------
1 | require "dry-container"
2 | require "formalist/rich_text/embedded_forms_container/registration"
3 |
4 | module Formalist
5 | module RichText
6 | class EmbeddedFormsContainer
7 | module Mixin
8 | def self.included(base)
9 | base.class_eval do
10 | include ::Dry::Container::Mixin
11 | include Methods
12 | end
13 | end
14 |
15 | def self.extended(base)
16 | base.class_eval do
17 | extend ::Dry::Container::Mixin
18 | extend Methods
19 | end
20 | end
21 |
22 | module Methods
23 | def resolve(key)
24 | super(key.to_s)
25 | end
26 |
27 | def register(key, **attrs)
28 | super(key.to_s, Registration.new(**attrs))
29 | end
30 |
31 | def to_h
32 | keys.each_with_object({}) { |key, output|
33 | output[key] = self[key].to_h
34 | }
35 | end
36 |
37 | # TODO: methods to return filtered sets of registrations
38 | end
39 | end
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/formalist/rich_text/embedded_forms_container/registration.rb:
--------------------------------------------------------------------------------
1 | module Formalist
2 | module RichText
3 | class EmbeddedFormsContainer
4 | class Registration
5 | DEFAULT_INPUT_PROCESSOR = -> input { input }.freeze
6 |
7 | attr_reader :label
8 | attr_reader :form
9 | attr_reader :schema
10 | attr_reader :input_processor
11 | attr_reader :preview_image_url
12 |
13 | def initialize(label:, form:, schema:, preview_image_url: nil, input_processor: DEFAULT_INPUT_PROCESSOR)
14 | @label = label
15 | @form = form
16 | @schema = schema
17 | @input_processor = input_processor
18 | @preview_image_url = preview_image_url
19 | end
20 |
21 | def to_h
22 | {
23 | label: label,
24 | form: form,
25 | schema: schema,
26 | input_processor: input_processor,
27 | preview_image_url: preview_image_url
28 | }
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/formalist/rich_text/rendering/embedded_form_renderer.rb:
--------------------------------------------------------------------------------
1 | module Formalist
2 | module RichText
3 | module Rendering
4 | class EmbeddedFormRenderer
5 | attr_reader :container
6 | attr_reader :namespace
7 | attr_reader :paths
8 | attr_reader :options
9 |
10 | def initialize(container = {}, namespace: nil, paths: [], **options)
11 | @container = container
12 | @namespace = namespace
13 | @paths = paths
14 | @options = options
15 | end
16 |
17 | def call(form_data)
18 | type, data = form_data.values_at(:name, :data)
19 |
20 | key = resolve_key(type)
21 |
22 | if key
23 | container[key].(data, **options)
24 | else
25 | ""
26 | end
27 | end
28 |
29 | def with(**context_options)
30 | self.class.new(
31 | container,
32 | namespace: namespace,
33 | paths: paths,
34 | **options.merge(context_options)
35 | )
36 | end
37 |
38 | private
39 |
40 | def resolve_key(type)
41 | paths.each do |path|
42 | path_key = path.tr("/", ".")
43 | key = [namespace, path_key, type].compact.join(".")
44 | return key if container.key?(key)
45 | end
46 |
47 | key = [namespace, type].compact.join(".")
48 | return key if container.key?(key)
49 | end
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/formalist/rich_text/rendering/html_compiler.rb:
--------------------------------------------------------------------------------
1 | module Formalist
2 | module RichText
3 | module Rendering
4 | class HTMLCompiler
5 | EMBEDDED_FORM_TYPE = "formalist".freeze
6 | LIST_ITEM_TYPES = %w[unordered-list-item ordered-list-item].freeze
7 |
8 | attr_reader :html_renderer
9 | attr_reader :embedded_form_renderer
10 |
11 | def initialize(html_renderer:, embedded_form_renderer:)
12 | @html_renderer = html_renderer
13 | @embedded_form_renderer = embedded_form_renderer
14 | end
15 |
16 | def call(ast)
17 | html_renderer.nodes(wrap_lists(ast)) do |node|
18 | visit(node)
19 | end
20 | end
21 |
22 | private
23 |
24 | def visit(node)
25 | type, content = node
26 |
27 | send(:"visit_#{type}", content)
28 | end
29 |
30 | def visit_block(data)
31 | type, key, children = data
32 |
33 | html_renderer.block(type, key, wrap_lists(children)) do |child|
34 | visit(child)
35 | end
36 | end
37 |
38 | def visit_wrapper(data)
39 | type, children = data
40 |
41 | html_renderer.wrapper(type, children) do |child|
42 | visit(child)
43 | end
44 | end
45 |
46 | def visit_inline(data)
47 | styles, text = data
48 |
49 | html_renderer.inline(styles, text)
50 | end
51 |
52 | def visit_entity(data)
53 | type, key, _mutability, data, children = data
54 |
55 | # FIXME
56 | # Temporary fix to handle data that comes through with keys as
57 | # strings instead of symbols
58 | data = data.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
59 |
60 | if type == EMBEDDED_FORM_TYPE
61 | embedded_form_renderer.(data)
62 | else
63 | html_renderer.entity(type, key, data, wrap_lists(children)) do |child|
64 | visit(child)
65 | end
66 | end
67 | end
68 |
69 | def wrap_lists(nodes)
70 | chunked = nodes.chunk do |node|
71 | type, content = node
72 |
73 | if type == "block"
74 | content[0] # return the block's own type
75 | else
76 | type
77 | end
78 | end
79 |
80 | chunked.inject([]) { |output, (type, chunk)|
81 | if list_item?(type)
82 | output << convert_to_wrapper_node(type, chunk)
83 | else
84 | # Flatten again by appending chunk onto array
85 | output + chunk
86 | end
87 | }
88 | end
89 |
90 | def convert_to_wrapper_node(type, children)
91 | ["wrapper", [type, children]]
92 | end
93 |
94 | def list_item?(type)
95 | LIST_ITEM_TYPES.include?(type)
96 | end
97 | end
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/lib/formalist/rich_text/rendering/html_renderer.rb:
--------------------------------------------------------------------------------
1 | module Formalist
2 | module RichText
3 | module Rendering
4 | class HTMLRenderer
5 | # A DraftJS HTML renderer must have the following rendering methods implemented:
6 | # 1. inline
7 | # 2. block
8 | # 3. entity
9 | # 4. wrapper
10 | # 5. nodes
11 |
12 | # block and entity must iterate over the children and yield each of the children back to the compiler
13 |
14 | BLOCK_ELEMENTS_MAP = {
15 | "header-one" => "h1",
16 | "header-two" => "h2",
17 | "header-three" => "h3",
18 | "header-four" => "h4",
19 | "header-five" => "h5",
20 | "header-six" => "h6",
21 | "unordered-list-item" => "li",
22 | "ordered-list-item" => "li",
23 | "blockquote" => "blockquote",
24 | "pullquote" => "aside",
25 | "code-block" => "pre",
26 | "horizontal-rule" => "hr",
27 | }.freeze
28 |
29 | DEFAULT_BLOCK_ELEMENT = "p".freeze
30 |
31 | INLINE_ELEMENTS_MAP = {
32 | "bold" => "strong",
33 | "italic" => "em",
34 | "strikethrough" => "del",
35 | "code" => "code",
36 | "underline" => "u",
37 | }
38 |
39 | DEFAULT_INLINE_ELEMENT = "span".freeze
40 |
41 | def initialize(options = {})
42 | @options = options
43 | end
44 |
45 | # Defines how to handle a list of nodes
46 | def nodes(nodes)
47 | nodes = nodes.map { |node| yield(node) } if block_given?
48 | nodes.join
49 | end
50 |
51 | # Defines how to handle a block node
52 | def block(type, key, children)
53 | rendered_children = children.map { |child| yield(child) }
54 |
55 | if type == 'atomic'
56 | block_atomic(key, rendered_children)
57 | else
58 | render_block_element(type, rendered_children)
59 | end
60 | end
61 |
62 | # Defines how to handle a list of blocks with a list type
63 | def wrapper(type, children)
64 | type_for_method = type.gsub("-", "_")
65 |
66 | rendered_children = children.map { |child| yield(child) }
67 |
68 | send(:"wrapper_#{type_for_method}", rendered_children)
69 | end
70 |
71 | def inline(styles, content)
72 | return content if styles.nil? || styles.empty?
73 | out = content
74 | styles.each do |style|
75 | out = render_inline_element(style, out)
76 | end
77 | out
78 | end
79 |
80 | def entity(type, key, data, children)
81 | rendered_children = children.map { |child| yield(child) }
82 |
83 | handler = :"entity_#{type.downcase}"
84 | if respond_to?(handler, _include_private=true)
85 | send(handler, data, rendered_children)
86 | else
87 | rendered_children
88 | end
89 | end
90 |
91 | private
92 |
93 | def block_atomic(key, children)
94 | children.join
95 | end
96 |
97 | def wrapper_unordered_list_item(children)
98 | html_tag(:ul) do
99 | children.join
100 | end
101 | end
102 |
103 | def wrapper_ordered_list_item(children)
104 | html_tag(:ol) do
105 | children.join
106 | end
107 | end
108 |
109 | def entity_link(data, children)
110 | link_attrs = {
111 | href: data[:url]
112 | }
113 | link_attrs = link_attrs.merge(
114 | target: "_blank",
115 | rel: "noopener"
116 | ) if data[:newWindow]
117 | html_tag(:a, link_attrs) do
118 | children.join
119 | end
120 | end
121 |
122 | def entity_image(data, children)
123 | html_tag(:img, src: data[:src])
124 | end
125 |
126 | def entity_video(data, children)
127 | html_tag(:video, src: data[:src])
128 | end
129 |
130 | def entity_default(attrs, children)
131 | html_tag(:div, attrs) do
132 | children.join
133 | end
134 | end
135 |
136 | def render_block_element(type, content)
137 | elem = BLOCK_ELEMENTS_MAP.fetch(type.downcase, DEFAULT_BLOCK_ELEMENT)
138 |
139 | html_tag(elem) do
140 | if content.is_a?(Array)
141 | content.join
142 | else
143 | content
144 | end
145 | end
146 | end
147 |
148 | def render_inline_element(type, content)
149 | elem = INLINE_ELEMENTS_MAP.fetch(type.downcase, DEFAULT_INLINE_ELEMENT)
150 |
151 | html_tag(elem, class: "inline--#{type.downcase}") do
152 | if content.is_a?(Array)
153 | content.join
154 | else
155 | content
156 | end
157 | end
158 | end
159 |
160 | def html_tag(tag, options = {})
161 | options_string = html_options_string(options)
162 | out = "<#{tag} #{options_string}".strip
163 |
164 | content = block_given? ? yield : ""
165 |
166 | if content.nil? || content.empty?
167 | out << "/>"
168 | else
169 | out << ">#{replace_soft_newlines(content)}#{tag}>"
170 | end
171 | end
172 |
173 | def html_options_string(options)
174 | opts = options.map do |key, val|
175 | "#{key}='#{val}'"
176 | end
177 | opts.join(" ")
178 | end
179 |
180 | def replace_soft_newlines(content)
181 | content.gsub(/\n/, '
')
182 | end
183 | end
184 | end
185 | end
186 | end
187 |
--------------------------------------------------------------------------------
/lib/formalist/rich_text/validity_check.rb:
--------------------------------------------------------------------------------
1 | require "formalist/form/validity_check"
2 |
3 | module Formalist
4 | module RichText
5 | class ValidityCheck
6 | AST = Struct.new(:ast)
7 |
8 | def call(ast)
9 | forms = ast.map { |node| visit(node) }.flatten
10 |
11 | form_validity_check = Form::ValidityCheck.new
12 | forms.all? { |form_ast| form_validity_check.(form_ast.ast) }
13 | end
14 | alias_method :[], :call
15 |
16 | private
17 |
18 | def visit(node)
19 | name, nodes = node
20 |
21 | handler = :"visit_#{name}"
22 |
23 | if respond_to?(handler, true)
24 | send(handler, nodes)
25 | else
26 | []
27 | end
28 | end
29 |
30 | # We need to visit blocks in order to get to the formalist entities nested within them
31 | def visit_block(node)
32 | type, id, children = node
33 |
34 | children.map { |child| visit(child) }
35 | end
36 |
37 | def visit_entity(node)
38 | type, key, mutability, entity_data, children = node
39 |
40 | if type == "formalist"
41 | [AST.new(entity_data["form"])]
42 | else
43 | []
44 | end
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/formalist/version.rb:
--------------------------------------------------------------------------------
1 | module Formalist
2 | VERSION = "0.9.0".freeze
3 | end
4 |
--------------------------------------------------------------------------------
/spec/integration/dependency_injection_spec.rb:
--------------------------------------------------------------------------------
1 | require "dry-auto_inject"
2 | require "formalist/elements/standard"
3 |
4 | RSpec.describe "Dependency injection" do
5 | let(:schema) {
6 | Dry::Validation.Schema do
7 | key(:status).required
8 | end
9 | }
10 |
11 | subject(:form) {
12 | Class.new(Formalist::Form) do
13 | define do
14 | select_box :status, options: status_options
15 | end
16 |
17 | attr_reader :status_repo
18 |
19 | def initialize(status_repo:, **args)
20 | @status_repo = status_repo
21 | super(**args)
22 | end
23 |
24 | def status_options
25 | status_repo.statuses.map { |status| [status, status.capitalize] }
26 | end
27 | end.new(status_repo: status_repo)
28 | }
29 |
30 | let(:status_repo) {
31 | Class.new do
32 | def statuses
33 | %w[draft published]
34 | end
35 | end.new
36 | }
37 |
38 | it "supports dependency injection via the initializer's options hash" do
39 | expect(form.to_ast).to eql [
40 | [:field, [
41 | :status,
42 | :select_box,
43 | nil,
44 | [],
45 | [:object, [
46 | [:options, [:array, [
47 | [:array, [[:value, ["draft"]], [:value, ["Draft"]]]],
48 | [:array, [[:value, ["published"]], [:value, ["Published"]]]]
49 | ]]]
50 | ]]
51 | ]]
52 | ]
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/spec/integration/form_spec.rb:
--------------------------------------------------------------------------------
1 | require "dry/validation/contract"
2 |
3 | RSpec.describe Formalist::Form do
4 | let(:contract) {
5 | Class.new(Dry::Validation::Contract) do
6 | schema do
7 | required(:title).filled(:string)
8 | required(:rating).filled(:integer)
9 |
10 | required(:reviews).array(:hash) do
11 | required(:summary).filled(:string)
12 | required(:rating).filled(:integer, gteq?: 1, lteq?: 10)
13 | end
14 |
15 | required(:meta).hash do
16 | required(:pages).filled(:integer, gteq?: 1)
17 | end
18 | end
19 | end.new
20 | }
21 |
22 | subject(:form_class) {
23 | Class.new(Formalist::Form) do
24 | define do
25 | compound_field do
26 | field :title, validate: {filled: true}
27 | field :rating, validate: {filled: true}
28 | end
29 |
30 | many :reviews do
31 | field :summary, validate: {filled: true}
32 | field :rating, validate: {filled: true}
33 | end
34 |
35 | attr :meta do
36 | field :pages, validate: {filled: true}
37 | end
38 | end
39 | end
40 | }
41 |
42 | let(:input) {
43 | {
44 | title: "Aurora",
45 | rating: "10",
46 | reviews: [
47 | {
48 | summary: "",
49 | rating: 10
50 | },
51 | {
52 | summary: "Great!",
53 | rating: 0
54 | }
55 | ],
56 | meta: {
57 | pages: 0
58 | }
59 | }
60 | }
61 |
62 | it "outputs an AST" do
63 | form = form_class.new.fill(input: input, errors: contract.(input).errors)
64 |
65 | expect(form.to_ast).to eq [
66 | [:compound_field, [
67 | :compound_field,
68 | [:object, []],
69 | [
70 | [:field, [:title, :field, "Aurora", [], [:object, []]]],
71 | [:field, [:rating, :field, "10", ["must be an integer"], [:object, []]]]
72 | ]
73 | ]],
74 | [:many, [
75 | :reviews,
76 | :many,
77 | [],
78 | [:object, [
79 | [:allow_create, [:value, [true]]],
80 | [:allow_update, [:value, [true]]],
81 | [:allow_destroy, [:value, [true]]],
82 | [:allow_reorder, [:value, [true]]]
83 | ]],
84 | [
85 | [:field, [:summary, :field, nil, [], [:object, []]]],
86 | [:field, [:rating, :field, nil, [], [:object, []]]]
87 | ],
88 | [
89 | [
90 | [:field, [:summary, :field, "", ["must be filled"], [:object, []]]],
91 | [:field, [:rating, :field, 10, [], [:object, []]]]
92 | ],
93 | [
94 | [:field, [:summary, :field, "Great!", [], [:object, []]]],
95 | [:field, [:rating, :field, 0, ["must be greater than or equal to 1"], [:object, []]]]
96 | ]
97 | ]
98 | ]],
99 | [:attr, [
100 | :meta,
101 | :attr,
102 | [],
103 | [:object, []],
104 | [
105 | [:field, [:pages, :field, 0, ["must be greater than or equal to 1"], [:object, []]]]
106 | ]
107 | ]]
108 | ]
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | if RUBY_ENGINE == "ruby"
2 | require "simplecov"
3 | SimpleCov.start do
4 | add_filter "/spec/"
5 | end
6 | end
7 |
8 | begin
9 | require "byebug"
10 | rescue LoadError; end
11 |
12 | require "formalist"
13 | require "dry-validation"
14 |
15 | # Requires supporting ruby files with custom matchers and macros, etc, in
16 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
17 | # run as spec files by default. This means that files in spec/support that end
18 | # in _spec.rb will both be required and run as specs, causing the specs to be
19 | # run twice. It is recommended that you do not name files matching this glob to
20 | # end with _spec.rb. You can configure this pattern with the --pattern
21 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
22 | #
23 | # The following line is provided for convenience purposes. It has the downside
24 | # of increasing the boot-up time by auto-requiring all files in the support
25 | # directory. Alternatively, in the individual `*_spec.rb` files, manually
26 | # require only the support files necessary.
27 | Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each do |f| require f end
28 |
29 | # This file was generated by the `rspec --init` command. Conventionally, all
30 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
31 | # The generated `.rspec` file contains `--require spec_helper` which will cause
32 | # this file to always be loaded, without a need to explicitly require it in any
33 | # files.
34 | #
35 | # Given that it is always loaded, you are encouraged to keep this file as
36 | # light-weight as possible. Requiring heavyweight dependencies from this file
37 | # will add to the boot time of your test suite on EVERY test run, even for an
38 | # individual file that may not need all of that loaded. Instead, consider making
39 | # a separate helper file that requires the additional dependencies and performs
40 | # the additional setup, and require it from the spec files that actually need
41 | # it.
42 | #
43 | # The `.rspec` file also contains a few flags that are not defaults but that
44 | # users commonly want.
45 | #
46 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
47 | RSpec.configure do |config|
48 | # rspec-expectations config goes here. You can use an alternate
49 | # assertion/expectation library such as wrong or the stdlib/minitest
50 | # assertions if you prefer.
51 | config.expect_with :rspec do |expectations|
52 | # This option will default to `true` in RSpec 4. It makes the `description`
53 | # and `failure_message` of custom matchers include text for helper methods
54 | # defined using `chain`, e.g.:
55 | # be_bigger_than(2).and_smaller_than(4).description
56 | # # => "be bigger than 2 and smaller than 4"
57 | # ...rather than:
58 | # # => "be bigger than 2"
59 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
60 | end
61 |
62 | # rspec-mocks config goes here. You can use an alternate test double
63 | # library (such as bogus or mocha) by changing the `mock_with` option here.
64 | config.mock_with :rspec do |mocks|
65 | # Prevents you from mocking or stubbing a method that does not exist on
66 | # a real object. This is generally recommended, and will default to
67 | # `true` in RSpec 4.
68 | mocks.verify_partial_doubles = true
69 | end
70 |
71 | # Allows RSpec to persist some state between runs in order to support
72 | # the `--only-failures` and `--next-failure` CLI options. We recommend
73 | # you configure your source control system to ignore this file.
74 | config.example_status_persistence_file_path = "spec/examples.txt"
75 |
76 | # Limits the available syntax to the non-monkey patched syntax that is
77 | # recommended.
78 | config.disable_monkey_patching!
79 |
80 | # This setting enables warnings. It's recommended, but in some cases may
81 | # be too noisy due to issues in dependencies.
82 | # config.warnings = true
83 | config.warnings = false
84 |
85 | # Many RSpec users commonly either run the entire suite or an individual
86 | # file, and it's useful to allow more verbose output when running an
87 | # individual spec file.
88 | if config.files_to_run.one?
89 | # Use the documentation formatter for detailed output,
90 | # unless a formatter has already been configured
91 | # (e.g. via a command-line flag).
92 | config.default_formatter = "doc"
93 | end
94 |
95 | # Run specs in random order to surface order dependencies. If you find an
96 | # order dependency and want to debug it, you can fix the order by providing
97 | # the seed, which is printed after each run.
98 | # --seed 1234
99 | config.order = :random
100 |
101 | # Seed global randomization in this process using the `--seed` CLI option.
102 | # Setting this allows you to use `--seed` to deterministically reproduce
103 | # test failures related to randomization by passing the same `--seed` value
104 | # as the one that triggered the failure.
105 | Kernel.srand config.seed
106 | end
107 |
--------------------------------------------------------------------------------
/spec/support/constants.rb:
--------------------------------------------------------------------------------
1 | module Test
2 | def self.remove_constants
3 | constants.each(&method(:remove_const))
4 | end
5 | end
6 |
7 | RSpec.configure do |config|
8 | config.after do
9 | Test.remove_constants
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/spec/unit/child_forms/builder_spec.rb:
--------------------------------------------------------------------------------
1 | require "dry/validation/contract"
2 | require "formalist/form"
3 | require "formalist/elements/standard"
4 | require "formalist/rich_text/embedded_forms_container"
5 | require "formalist/child_forms/builder"
6 |
7 | RSpec.describe Formalist::ChildForms::Builder do
8 | let(:builder) { described_class.new(embedded_forms) }
9 |
10 | let(:embedded_forms) {
11 | Formalist::RichText::EmbeddedFormsContainer.new.tap do |collection|
12 | collection.register :image_with_caption, label: "Image with caption", form: form, schema: schema
13 | end
14 | }
15 |
16 | let(:form) {
17 | Class.new(Formalist::Form) do
18 | define do
19 | text_field :image_id
20 | text_field :caption
21 | end
22 | end.new
23 | }
24 |
25 | let(:schema) {
26 | Class.new(Dry::Validation::Contract) do
27 | params do
28 | required(:image_id).filled(:integer)
29 | required(:caption).filled(:string)
30 | end
31 | end.new
32 | }
33 |
34 | describe "valid data" do
35 | let(:input) {
36 | [
37 | {:name => "image_with_caption",:label => "Image with caption",:data => {"image_id" => 5678,"caption" => "Large panda"}},
38 | ]
39 | }
40 |
41 | let(:output) {
42 | builder.(input)
43 | }
44 |
45 | it "builds a list of form elements with input AST containing form data" do
46 | expect(output.count).to eq 1
47 | expect(output.first).to be_a_kind_of(Formalist::ChildForms::ChildForm)
48 | expect(output.first.input).to eq([[:field, [:image_id, :text_field, 5678, [], [:object, []]]], [:field, [:caption, :text_field, "Large panda", [], [:object, []]]]])
49 | end
50 | end
51 |
52 | describe "invalid data" do
53 | let(:input) {
54 | [
55 | {:name => "image_with_caption",:label => "Image with caption",:data => {"image_id" => "","caption" => "Large panda"}},
56 | ]
57 | }
58 |
59 | let(:output) {
60 | builder.(input)
61 | }
62 |
63 | it "builds a list of form elements with input ast containing form data and errors" do
64 | expect(output.count).to eq 1
65 | expect(output.first).to be_a_kind_of(Formalist::ChildForms::ChildForm)
66 | expect(output.first.input).to eq([[:field, [:image_id, :text_field, "", ["must be filled"], [:object, []]]], [:field, [:caption, :text_field, "Large panda", [], [:object, []]]]])
67 | end
68 | end
69 |
70 | describe "missing form definition" do
71 | let(:input) {
72 | [
73 | {:name => "deprecated_form",:label => "Old Image with caption", :data => {"image_url" => "/assets/large-panda", "caption" => "Large panda"}},
74 | ]
75 | }
76 |
77 | it "raises a missing form definition error" do
78 | expect { builder.(input).to raise_error(MissingFormDefinitionError) }
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/spec/unit/child_forms/child_form_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "formalist/child_forms/child_form"
3 | require "dry/validation/contract"
4 |
5 | RSpec.describe Formalist::ChildForms::ChildForm do
6 | subject(:child_form) {
7 | Formalist::ChildForms::ChildForm.new(
8 | name: :cta,
9 | attributes: attributes,
10 | ).fill(
11 | input: input,
12 | errors: errors,
13 | )
14 | }
15 |
16 | let(:form) {
17 | Class.new(Formalist::Form) do
18 | define do
19 | field :image_id
20 | field :caption
21 | end
22 | end.new
23 | }
24 |
25 | let(:schema) {
26 | Class.new(Dry::Validation::Contract) do
27 | params do
28 | required(:image_id).filled(:integer)
29 | required(:caption).filled(:string)
30 | end
31 | end.new
32 | }
33 |
34 | let(:attributes) { {label: "Call to action", form: form, schema: schema} }
35 | let(:errors) { {} }
36 |
37 | describe "input" do
38 | context "is valid form data" do
39 | let(:input) { {image_id: 12, caption: "A cute panda"} }
40 |
41 | it "converts the input data to valid form AST" do
42 | expect(child_form.input).to eql [
43 | [:field, [:image_id, :field, 12, [], [:object, []]]],
44 | [:field, [:caption, :field, "A cute panda", [], [:object, []]]]
45 | ]
46 | end
47 | end
48 |
49 | context "is invalid form data" do
50 | let(:input) { {image_id: nil, caption: "A cute panda"} }
51 |
52 | it "converts the input data to valid form AST with validation errors" do
53 | expect(child_form.input).to eql [
54 | [:field, [:image_id, :field, nil, ["must be filled"], [:object, []]]],
55 | [:field, [:caption, :field, "A cute panda", [], [:object, []]]]
56 | ]
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/unit/child_forms/params_processor_spec.rb:
--------------------------------------------------------------------------------
1 | require "dry/validation/contract"
2 | require "formalist/child_forms/params_processor"
3 |
4 | RSpec.describe Formalist::ChildForms::ParamsProcessor do
5 | subject(:processor) { described_class.new(embeddable_forms) }
6 |
7 | let(:embedded_form) { double(:embedded_form) }
8 |
9 | let(:contract) {
10 | Class.new(Dry::Validation::Contract) do
11 | params do
12 | required(:title).filled(:string)
13 | required(:visible).filled(:bool)
14 | end
15 | end.new
16 | }
17 |
18 | let(:embeddable_forms) {
19 | {call_to_action: embedded_form}
20 | }
21 |
22 | before do
23 | allow(embedded_form).to receive(:schema).and_return(contract)
24 | end
25 |
26 | describe "#call" do
27 | subject(:result) { processor.(input) }
28 |
29 | context "valid forms list" do
30 | let(:input) {
31 | [
32 | {
33 | name: :call_to_action,
34 | label: "Call to action",
35 | data: {
36 | title: "Find out more!",
37 | visible: "true"
38 | }
39 | },
40 | {
41 | name: :call_to_action,
42 | label: "Call to action",
43 | data: {
44 | title: "Another call!",
45 | visible: "false"
46 | }
47 | }
48 | ]
49 | }
50 |
51 | it { is_expected.to eql([
52 | {
53 | name: :call_to_action,
54 | label: "Call to action",
55 | data: {
56 | title: "Find out more!",
57 | visible: true
58 | }
59 | },
60 | {
61 | name: :call_to_action,
62 | label: "Call to action",
63 | data: {
64 | title: "Another call!",
65 | visible: false
66 | }
67 | }
68 | ])
69 |
70 | }
71 | end
72 |
73 | context "at least one invalid form input" do
74 | let(:input) {
75 | [
76 | {
77 | name: :call_to_action,
78 | label: "Call to action",
79 | data: {
80 | title: "Find out more!",
81 | visible: true
82 | }
83 | },
84 | {
85 | name: :call_to_action,
86 | label: "Call to action",
87 | data: {
88 | title: ""
89 | }
90 | },
91 | ]
92 | }
93 |
94 | it { is_expected.to eq(
95 | [
96 | {
97 | name: :call_to_action,
98 | label: "Call to action",
99 | data: {
100 | title: "Find out more!",
101 | visible: true
102 | }
103 | },
104 | {
105 | name: :call_to_action,
106 | label: "Call to action",
107 | data: {
108 | title: ""
109 | }
110 | },
111 | ]
112 | )}
113 | end
114 |
115 | context "empty input" do
116 | let(:input) { [] }
117 |
118 | it { is_expected.to eq [] }
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/spec/unit/child_forms/validity_check_spec.rb:
--------------------------------------------------------------------------------
1 | require "dry/validation/contract"
2 | require "formalist/child_forms/validity_check"
3 |
4 | RSpec.describe Formalist::ChildForms::ValidityCheck do
5 | subject(:check) { described_class.new(embeddable_forms) }
6 |
7 | let(:embedded_form) { double(:embedded_form) }
8 |
9 | let(:contract) {
10 | Class.new(Dry::Validation::Contract) do
11 | schema do
12 | required(:title).filled(:string)
13 | end
14 | end.new
15 | }
16 |
17 | let(:embeddable_forms) {
18 | {call_to_action: embedded_form}
19 | }
20 |
21 | before do
22 | allow(embedded_form).to receive(:schema).and_return(contract)
23 | end
24 |
25 | describe "#call" do
26 | subject(:result) { check.(input) }
27 |
28 | context "valid forms list" do
29 | let(:input) {
30 | [
31 | {
32 | name: :call_to_action,
33 | label: "Call to action",
34 | data: {
35 | title: "Find out more!"
36 | }
37 | },
38 | {
39 | name: :call_to_action,
40 | label: "Call to action",
41 | data: {
42 | title: "Another call!"
43 | }
44 | }
45 | ]
46 | }
47 |
48 | it { is_expected.to be true }
49 | end
50 |
51 | context "at least one invalid form input" do
52 | let(:input) {
53 | [
54 | {
55 | name: :call_to_action,
56 | label: "Call to action",
57 | data: {
58 | title: "Find out more!"
59 | }
60 | },
61 | {
62 | name: :call_to_action,
63 | label: "Call to action",
64 | data: {
65 | title: ""
66 | }
67 | },
68 | ]
69 | }
70 |
71 | it { is_expected.to be false }
72 | end
73 |
74 | context "empty input" do
75 | let(:input) { [] }
76 |
77 | it { is_expected.to be true }
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/spec/unit/elements/many_forms_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "formalist/elements/many_forms"
3 | require "dry/validation/contract"
4 | require "formalist/rich_text/embedded_forms_container"
5 |
6 |
7 | RSpec.describe Formalist::Elements::ManyForms do
8 | subject(:many_forms) {
9 | Formalist::Elements::ManyForms.new(
10 | name: :components,
11 | attributes: attributes,
12 | ).fill(
13 | input: input,
14 | errors: errors,
15 | )
16 | }
17 |
18 | let(:child_form) {
19 | Class.new(Formalist::Form) do
20 | define do
21 | field :image_id
22 | field :caption
23 | end
24 | end.new
25 | }
26 |
27 | let(:child_schema) {
28 | Class.new(Dry::Validation::Contract) do
29 | params do
30 | required(:image_id).filled(:integer)
31 | required(:caption).filled(:string)
32 | end
33 | end.new
34 | }
35 |
36 | let(:embedded_forms) {
37 | Formalist::RichText::EmbeddedFormsContainer.new.tap do |collection|
38 | collection.register :image_with_caption, label: "Image with caption", form: child_form, schema: child_schema
39 | end
40 | }
41 |
42 | let(:attributes) { {label: "Components", embeddable_forms: embedded_forms} }
43 | let(:errors) { {} }
44 |
45 | describe "input" do
46 | context "is valid form data" do
47 | let(:input) {
48 | {
49 | title: "Aurora",
50 | components: [
51 | {
52 | name: :image_with_caption,
53 | label: "Image with caption",
54 | data: {
55 | image_id: 1234,
56 | caption: "Cute cat"
57 | }
58 | }
59 | ],
60 | }
61 | }
62 |
63 | it "converts the input data to valid form AST" do
64 | expect(many_forms.children.count).to eql 1
65 |
66 | expect(many_forms.children.first.input).to eql [
67 | [:field, [:image_id, :field, 1234, [], [:object, []]]],
68 | [:field, [:caption, :field, "Cute cat", [], [:object, []]]]
69 | ]
70 | end
71 | end
72 |
73 | context "is invalid form data" do
74 | let(:input) {
75 | {
76 | title: "Aurora",
77 | components: [
78 | {
79 | name: :image_with_caption,
80 | label: "Image with caption",
81 | data: {
82 | image_id: nil,
83 | caption: "Cute cat"
84 | }
85 | }
86 | ],
87 | }
88 | }
89 |
90 | it "converts the input data to valid form AST with validation errors" do
91 | expect(many_forms.children.first.input).to eql [
92 | [:field, [:image_id, :field, nil, ["must be filled"], [:object, []]]],
93 | [:field, [:caption, :field, "Cute cat", [], [:object, []]]]
94 | ]
95 | end
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/spec/unit/elements/standard/check_box_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "formalist/elements/standard/check_box"
3 |
4 | RSpec.describe Formalist::Elements::CheckBox do
5 | subject(:check_box) {
6 | Formalist::Elements::CheckBox.new(
7 | name: :published,
8 | attributes: attributes,
9 | ).fill(
10 | input: {published: input_value},
11 | errors: errors,
12 | )
13 | }
14 |
15 | let(:attributes) { {} }
16 | let(:errors) { {} }
17 |
18 | describe "input" do
19 | context "is nil" do
20 | let(:input_value) { nil }
21 |
22 | specify { expect(check_box.input).to eql false }
23 | end
24 |
25 | context "is false" do
26 | let(:input_value) { false }
27 |
28 | specify { expect(check_box.input).to eql false }
29 | end
30 |
31 | context "is true" do
32 | let(:input_value) { true }
33 |
34 | specify { expect(check_box.input).to eql true }
35 | end
36 |
37 | context "is any other value" do
38 | let(:input_value) { "something" }
39 |
40 | specify { expect(check_box.input).to eql true }
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/spec/unit/form/validity_check_spec.rb:
--------------------------------------------------------------------------------
1 | require "formalist/elements/standard"
2 | require "formalist/form/validity_check"
3 |
4 | RSpec.describe Formalist::Form::ValidityCheck do
5 | subject(:check) { described_class.new }
6 |
7 | subject(:form_ast) {
8 | form = Class.new(Formalist::Form) do
9 | define do
10 | text_field :title
11 | end
12 | end.new.fill(input: input, errors: errors).to_ast
13 | }
14 |
15 | describe "#call" do
16 | subject(:result) { check.(form_ast) }
17 |
18 | context "valid form" do
19 | let(:input) {
20 | {title: "The Martian"}
21 | }
22 |
23 | let(:errors) { {} }
24 |
25 | it { is_expected.to be true }
26 | end
27 |
28 | context "invalid form" do
29 | let(:input) {
30 | {title: "The Martian"}
31 | }
32 |
33 | let(:errors) {
34 | {title: ["Must be Terran"]}
35 | }
36 |
37 | it { is_expected.to be false }
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/unit/rich_text/embedded_form_compiler_spec.rb:
--------------------------------------------------------------------------------
1 | require "dry/schema"
2 | require "formalist/form"
3 | require "formalist/elements/standard"
4 | require "formalist/rich_text/embedded_forms_container"
5 | require "formalist/rich_text/embedded_form_compiler"
6 |
7 | RSpec.describe Formalist::RichText::EmbeddedFormCompiler do
8 | let(:compiler) { described_class.new(embedded_forms) }
9 |
10 | let(:embedded_forms) {
11 | Formalist::RichText::EmbeddedFormsContainer.new.tap do |collection|
12 | collection.register :image_with_caption, label: "Image with caption", form: form, schema: schema
13 | end
14 | }
15 |
16 | let(:form) {
17 | Class.new(Formalist::Form) do
18 | define do
19 | text_field :image_id
20 | text_field :caption
21 | end
22 | end.new
23 | }
24 |
25 | let(:schema) {
26 | Dry::Schema.Params do
27 | required(:image_id).filled(:integer)
28 | required(:caption).filled(:string)
29 | end
30 | }
31 |
32 | describe "valid data" do
33 | let(:input) {
34 | [
35 | ["block",["unstyled","b14hd",[["inline",[[],"Before!"]]]]],
36 | ["block",["atomic","48b4f",[["entity",["formalist","1","IMMUTABLE",{"name" => "image_with_caption","label" => "Image with caption","data" => {"image_id" => 5678,"caption" => "Large panda"}},[["inline",[[],"¶"]]]]]]]],
37 | ["block",["unstyled","aivqi",[["inline",[[],"After!"]]]]]
38 | ]
39 | }
40 |
41 | let(:output) {
42 | compiler.(input)
43 | }
44 |
45 | it "builds a form AST with the data incorporated" do
46 | expect(output[1]).to eq ["block", ["atomic", "48b4f", [["entity", ["formalist", "1", "IMMUTABLE", {"name" => "image_with_caption", "label" => "Image with caption", "data" => {"image_id" => 5678, "caption" => "Large panda"}, "form" => [[:field, [:image_id, :text_field, 5678, [], [:object, []]]], [:field, [:caption, :text_field, "Large panda", [], [:object, []]]]]}, [["inline", [[], "¶"]]]]]]]]
47 | end
48 |
49 | it "leaves the rest of the data unchanged" do
50 | expect(output[0]).to eq input[0]
51 | expect(output[2]).to eq input[2]
52 | expect(output.length).to eq 3
53 | end
54 | end
55 |
56 | describe "invalid data" do
57 | let(:input) {
58 | [
59 | ["block",["unstyled","b14hd",[["inline",[[],"Before!"]]]]],
60 | ["block",["atomic","48b4f",[["entity",["formalist","1","IMMUTABLE",{"name" => "image_with_caption","label" => "Image with caption","data" => {"image_id" => "","caption" => "Large panda"}},[["inline",[[],"¶"]]]]]]]],
61 | ["block",["unstyled","aivqi",[["inline",[[],"After!"]]]]]
62 | ]
63 | }
64 |
65 | let(:output) {
66 | compiler.(input)
67 | }
68 |
69 | it "builds a form AST with the data and validation messages incorporated" do
70 | expect(output[1]).to eq ["block", ["atomic", "48b4f", [["entity", ["formalist", "1", "IMMUTABLE", {"name" => "image_with_caption", "label" => "Image with caption", "data" => {"image_id" => "", "caption" => "Large panda"}, "form" => [[:field, [:image_id, :text_field, "", ["must be filled"], [:object, []]]], [:field, [:caption, :text_field, "Large panda", [], [:object, []]]]]}, [["inline", [[], "¶"]]]]]]]]
71 | end
72 |
73 | it "leaves the rest of the data unchanged" do
74 | expect(output[0]).to eq input[0]
75 | expect(output[2]).to eq input[2]
76 | expect(output.length).to eq 3
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/spec/unit/rich_text/rendering/embedded_form_renderer_spec.rb:
--------------------------------------------------------------------------------
1 | require "formalist/rich_text/rendering/embedded_form_renderer"
2 |
3 | RSpec.describe Formalist::RichText::Rendering::EmbeddedFormRenderer do
4 | subject(:renderer) { described_class.new(container, namespace: namespace, paths: paths, **options) }
5 |
6 | let(:container) {{
7 | "article" => -> (*_) { "top_level_article" },
8 | "embedded_forms.article" => -> (*_) { "namespaced_article" },
9 | "embedded_forms.newsletter.article" => -> (*_) { "newsletter_article" },
10 | "embedded_forms.newsletter.components.article" => -> (*_) { "newsletter_components_article" },
11 | "embedded_forms.general.article" => -> (*_) { "general_article" },
12 | }}
13 |
14 | let(:options) {
15 | { render_context: "general" }
16 | }
17 | let(:namespace) { nil }
18 | let(:paths) { [] }
19 |
20 | describe "#call" do
21 | context "no namespace or paths configured" do
22 | let(:namespace) { nil }
23 | let(:paths) { [] }
24 |
25 | it "returns the top level result" do
26 | expect(renderer.(name: "article")).to eq "top_level_article"
27 | end
28 | end
29 |
30 |
31 | context "namespace configured" do
32 | let(:namespace) { "embedded_forms" }
33 | let(:paths) { [] }
34 |
35 | it "returns the namespaced result" do
36 | expect(renderer.(name: "article")).to eq "namespaced_article"
37 | end
38 | end
39 |
40 | context "namespace and paths configured" do
41 | let(:namespace) { "embedded_forms" }
42 | let(:paths) { ["newsletter/components", "newsletter", "general"] }
43 |
44 | it "returns the result in the first path" do
45 | expect(renderer.(name: "article")).to eq "newsletter_components_article"
46 | end
47 | end
48 |
49 | context "key not in paths" do
50 | let(:namespace) { "embedded_forms" }
51 | let(:paths) { ["other", "path"] }
52 |
53 | it "returns the result in the namespace" do
54 | expect(renderer.(name: "article")).to eq "namespaced_article"
55 | end
56 | end
57 |
58 | context "key not in namespace" do
59 | let(:namespace) { "embedded_forms" }
60 | let(:paths) { ["other", "path"] }
61 | let(:container) { { "article" => "top_level_article" } }
62 |
63 | it "does not return the result outside the namespace" do
64 | expect(renderer.(name: "article")).to eq ""
65 | end
66 | end
67 | end
68 |
69 | describe "#with" do
70 | it "returns an object with options merged" do
71 | expect(renderer.with(context: "new").options).to eq({render_context: "general", context: "new"})
72 | end
73 | end
74 | end
--------------------------------------------------------------------------------
/spec/unit/rich_text/rendering/html_compiler_spec.rb:
--------------------------------------------------------------------------------
1 | require "formalist/rich_text/rendering/html_compiler"
2 | require "formalist/rich_text/rendering/html_renderer"
3 | require "formalist/rich_text/rendering/embedded_form_renderer"
4 |
5 | RSpec.describe Formalist::RichText::Rendering::HTMLCompiler do
6 | let(:ast) {
7 | block1 = ["block", ["header-two", "a34sd", [["inline", [[], "Heading"]]]]]
8 | block2 = ["block", ["unordered-list-item", "dodnk", [["inline", [[], "I am more "]], ["entity", ["LINK", "3", "MUTABLE", {url: "http://makenosound.com"}, [["inline", [[], "content"]]]]], ["inline", [[], "."]]]]]
9 |
10 | [block1, block2, block2, block1]
11 | }
12 |
13 | describe "with default HTML renderer" do
14 | let(:compiler) {
15 | described_class.new(
16 | html_renderer: Formalist::RichText::Rendering::HTMLRenderer.new,
17 | embedded_form_renderer: Formalist::RichText::Rendering::EmbeddedFormRenderer.new,
18 | )
19 | }
20 |
21 | describe "#call" do
22 | subject(:html) { compiler.(ast) }
23 |
24 | it "compiles to HTML" do
25 | is_expected.to eq <<-HTML.gsub(/^\s+/, "").gsub("\n", "")
26 |