formalist 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +7 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +21 -0
  5. data/.yardopts +1 -0
  6. data/CHANGELOG.md +62 -0
  7. data/Gemfile +1 -2
  8. data/bin/console +13 -0
  9. data/formalist.gemspec +33 -0
  10. data/lib/formalist/definition.rb +65 -0
  11. data/lib/formalist/element/class_interface.rb +7 -59
  12. data/lib/formalist/element.rb +37 -19
  13. data/lib/formalist/elements/attr.rb +8 -20
  14. data/lib/formalist/elements/compound_field.rb +5 -4
  15. data/lib/formalist/elements/field.rb +5 -12
  16. data/lib/formalist/elements/group.rb +6 -5
  17. data/lib/formalist/elements/many.rb +28 -29
  18. data/lib/formalist/elements/section.rb +6 -10
  19. data/lib/formalist/elements/standard/multi_upload_field.rb +8 -0
  20. data/lib/formalist/elements/standard/rich_text_area.rb +40 -0
  21. data/lib/formalist/elements/standard/search_multi_selection_field.rb +20 -0
  22. data/lib/formalist/elements/standard/search_selection_field.rb +20 -0
  23. data/lib/formalist/elements/standard/tags_field.rb +16 -0
  24. data/lib/formalist/elements/standard/upload_field.rb +8 -0
  25. data/lib/formalist/elements/standard.rb +4 -0
  26. data/lib/formalist/form/validity_check.rb +54 -0
  27. data/lib/formalist/form.rb +49 -17
  28. data/lib/formalist/rich_text/embedded_form_compiler.rb +86 -0
  29. data/lib/formalist/rich_text/embedded_forms_container/mixin.rb +42 -0
  30. data/lib/formalist/rich_text/embedded_forms_container/registration.rb +30 -0
  31. data/lib/formalist/rich_text/embedded_forms_container.rb +9 -0
  32. data/lib/formalist/rich_text/rendering/embedded_form_renderer.rb +25 -0
  33. data/lib/formalist/rich_text/rendering/html_compiler.rb +100 -0
  34. data/lib/formalist/rich_text/rendering/html_renderer.rb +186 -0
  35. data/lib/formalist/rich_text/validity_check.rb +48 -0
  36. data/lib/formalist/types.rb +8 -7
  37. data/lib/formalist/version.rb +1 -1
  38. metadata +54 -31
  39. data/Gemfile.lock +0 -105
  40. data/lib/formalist/element/definition.rb +0 -55
  41. data/lib/formalist/element/permitted_children.rb +0 -46
  42. data/lib/formalist/form/definition_context.rb +0 -69
  43. data/lib/formalist/form/result.rb +0 -24
  44. data/spec/examples.txt +0 -8
  45. data/spec/integration/dependency_injection_spec.rb +0 -54
  46. data/spec/integration/form_spec.rb +0 -104
  47. data/spec/spec_helper.rb +0 -109
  48. data/spec/support/constants.rb +0 -11
  49. data/spec/unit/elements/standard/check_box_spec.rb +0 -33
@@ -0,0 +1,40 @@
1
+ require "formalist/element"
2
+ require "formalist/elements"
3
+ require "formalist/types"
4
+ require "formalist/rich_text/embedded_form_compiler"
5
+
6
+ module Formalist
7
+ class Elements
8
+ class RichTextArea < Field
9
+ attribute :box_size, Types::String.enum("single", "small", "normal", "large", "xlarge"), default: "normal"
10
+ attribute :inline_formatters, Types::Array
11
+ attribute :block_formatters, Types::Array
12
+ attribute :embeddable_forms, Types::Dependency.constrained(respond_to: :to_h)
13
+
14
+ # FIXME: it would be tidier to have a reader method for each attribute
15
+ def attributes
16
+ super.merge(embeddable_forms: embeddable_forms_config)
17
+ end
18
+
19
+ def input
20
+ input_compiler.(@input)
21
+ end
22
+
23
+ private
24
+
25
+ # Replace the form objects with their AST
26
+ def embeddable_forms_config
27
+ @attributes[:embeddable_forms].to_h.map { |key, attrs|
28
+ [key, attrs.merge(form: attrs[:form].to_ast)]
29
+ }.to_h
30
+ end
31
+
32
+ # TODO: make compiler configurable somehow?
33
+ def input_compiler
34
+ RichText::EmbeddedFormCompiler.new(@attributes[:embeddable_forms])
35
+ end
36
+ end
37
+
38
+ register :rich_text_area, RichTextArea
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ require "formalist/element"
2
+ require "formalist/elements"
3
+ require "formalist/types"
4
+
5
+ module Formalist
6
+ class Elements
7
+ class SearchMultiSelectionField < Field
8
+ attribute :selector_label, Types::String
9
+ attribute :render_option_as, Types::String
10
+ attribute :render_selection_as, Types::String
11
+ attribute :search_url, Types::String
12
+ attribute :search_per_page, Types::Int
13
+ attribute :search_params, Types::Hash
14
+ attribute :search_threshold, Types::Int
15
+ attribute :selections, Types::Array
16
+ end
17
+
18
+ register :search_multi_selection_field, SearchMultiSelectionField
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require "formalist/element"
2
+ require "formalist/elements"
3
+ require "formalist/types"
4
+
5
+ module Formalist
6
+ class Elements
7
+ class SearchSelectionField < Field
8
+ attribute :selector_label, Types::String
9
+ attribute :render_option_as, Types::String
10
+ attribute :render_selection_as, Types::String
11
+ attribute :search_url, Types::String
12
+ attribute :search_per_page, Types::Int
13
+ attribute :search_params, Types::Hash
14
+ attribute :search_threshold, Types::Int
15
+ attribute :selection, Types::Hash
16
+ end
17
+
18
+ register :search_selection_field, SearchSelectionField
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ require "formalist/element"
2
+ require "formalist/elements"
3
+ require "formalist/types"
4
+
5
+ module Formalist
6
+ class Elements
7
+ class TagsField < Field
8
+ attribute :search_url, Types::String
9
+ attribute :search_per_page, Types::Int
10
+ attribute :search_params, Types::Hash
11
+ attribute :search_threshold, Types::Int
12
+ end
13
+
14
+ register :tags_field, TagsField
15
+ end
16
+ end
@@ -6,6 +6,14 @@ module Formalist
6
6
  class Elements
7
7
  class UploadField < Field
8
8
  attribute :presign_url, Types::String
9
+ attribute :presign_options, Types::Hash
10
+ attribute :render_uploaded_as, Types::String
11
+ attribute :upload_prompt, Types::String
12
+ attribute :upload_action_label, Types::String
13
+ attribute :max_file_size, Types::String
14
+ attribute :max_file_size_message, Types::String
15
+ attribute :permitted_file_type_message, Types::String
16
+ attribute :permitted_file_type_regex, Types::String
9
17
  end
10
18
 
11
19
  register :upload_field, UploadField
@@ -6,8 +6,12 @@ require "formalist/elements/standard/multi_selection_field"
6
6
  require "formalist/elements/standard/multi_upload_field"
7
7
  require "formalist/elements/standard/number_field"
8
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"
9
12
  require "formalist/elements/standard/select_box"
10
13
  require "formalist/elements/standard/selection_field"
14
+ require "formalist/elements/standard/tags_field"
11
15
  require "formalist/elements/standard/text_area"
12
16
  require "formalist/elements/standard/text_field"
13
17
  require "formalist/elements/standard/upload_field"
@@ -0,0 +1,54 @@
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
+ errors.empty? && children.map { |child| visit(child) }.all?
45
+ end
46
+
47
+ def visit_section(node)
48
+ _name, _type, _attributes, children = node
49
+
50
+ children.map { |child| visit(child) }.all?
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,32 +1,64 @@
1
- require "dry-configurable"
1
+ require "dry/configurable"
2
+ require "dry/core/constants"
2
3
  require "formalist/elements"
3
- require "formalist/form/definition_context"
4
- require "formalist/element/permitted_children"
5
- require "formalist/form/result"
4
+ require "formalist/definition"
6
5
 
7
6
  module Formalist
8
7
  class Form
9
8
  extend Dry::Configurable
9
+ include Dry::Core::Constants
10
10
 
11
11
  setting :elements_container, Elements
12
12
 
13
- # @api private
14
- def self.elements
15
- @elements ||= []
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
+ )
16
49
  end
17
50
 
18
- # @api public
19
- def self.define(&block)
20
- @elements = DefinitionContext.new(
21
- container: config.elements_container,
22
- permissions: Element::PermittedChildren.all
23
- ).call(&block).elements
51
+ def with(**new_dependencies)
52
+ self.class.new(
53
+ elements: @elements,
54
+ input: @input,
55
+ errors: @errors,
56
+ **@dependencies.merge(new_dependencies)
57
+ )
24
58
  end
25
59
 
26
- # @api public
27
- def build(input = {}, messages = {})
28
- elements = self.class.elements.map { |el| el.resolve(self) }
29
- Result.new(input, messages, elements)
60
+ def to_ast
61
+ elements.map(&:to_ast)
30
62
  end
31
63
  end
32
64
  end
@@ -0,0 +1,86 @@
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.messages).to_ast
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,42 @@
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
@@ -0,0 +1,30 @@
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
+
12
+ def initialize(label:, form:, schema:, input_processor: DEFAULT_INPUT_PROCESSOR)
13
+ @label = label
14
+ @form = form
15
+ @schema = schema
16
+ @input_processor = input_processor
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ label: label,
22
+ form: form,
23
+ schema: schema,
24
+ input_processor: input_processor,
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,25 @@
1
+ module Formalist
2
+ module RichText
3
+ module Rendering
4
+ class EmbeddedFormRenderer
5
+ attr_reader :container
6
+ attr_reader :options
7
+
8
+ def initialize(container = {}, **options)
9
+ @container = container
10
+ @options = options
11
+ end
12
+
13
+ def call(form_data)
14
+ type, data = form_data.values_at(:name, :data)
15
+
16
+ if container.key?(type)
17
+ container[type].(data, options)
18
+ else
19
+ ""
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,100 @@
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