tailmix 0.2.0 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +147 -85
- data/app/javascript/tailmix/finder.js +12 -0
- data/app/javascript/tailmix/index.js +7 -0
- data/app/javascript/tailmix/mutator.js +28 -0
- data/app/javascript/tailmix/runner.js +7 -0
- data/app/javascript/tailmix/stimulus_adapter.js +37 -0
- data/examples/_modal_component.arb +36 -0
- data/examples/modal_component.rb +180 -0
- data/lib/generators/tailmix/install_generator.rb +19 -0
- data/lib/tailmix/definition/context_builder.rb +39 -0
- data/lib/tailmix/definition/contexts/action_builder.rb +31 -0
- data/lib/tailmix/definition/contexts/actions/element_builder.rb +30 -0
- data/lib/tailmix/definition/contexts/attribute_builder.rb +21 -0
- data/lib/tailmix/definition/contexts/dimension_builder.rb +16 -0
- data/lib/tailmix/definition/contexts/element_builder.rb +41 -0
- data/lib/tailmix/definition/contexts/stimulus_builder.rb +101 -0
- data/lib/tailmix/definition/merger.rb +93 -0
- data/lib/tailmix/definition/result.rb +31 -0
- data/lib/tailmix/definition.rb +11 -0
- data/lib/tailmix/dev/docs.rb +82 -0
- data/lib/tailmix/dev/stimulus_generator.rb +124 -0
- data/lib/tailmix/dev/tools.rb +26 -0
- data/lib/tailmix/engine.rb +17 -0
- data/lib/tailmix/html/attributes.rb +71 -0
- data/lib/tailmix/html/class_list.rb +79 -0
- data/lib/tailmix/html/data_map.rb +95 -0
- data/lib/tailmix/html/stimulus_builder.rb +65 -0
- data/lib/tailmix/runtime/action.rb +51 -0
- data/lib/tailmix/runtime/context.rb +66 -0
- data/lib/tailmix/runtime/facade_builder.rb +23 -0
- data/lib/tailmix/runtime/stimulus/compiler.rb +59 -0
- data/lib/tailmix/runtime.rb +14 -0
- data/lib/tailmix/version.rb +1 -1
- data/lib/tailmix.rb +48 -19
- metadata +33 -12
- data/examples/interactive_component.rb +0 -42
- data/examples/status_badge_component.rb +0 -44
- data/lib/tailmix/action.rb +0 -27
- data/lib/tailmix/dimension.rb +0 -18
- data/lib/tailmix/element.rb +0 -24
- data/lib/tailmix/manager.rb +0 -86
- data/lib/tailmix/part.rb +0 -39
- data/lib/tailmix/resolver.rb +0 -28
- data/lib/tailmix/schema.rb +0 -41
- data/lib/tailmix/utils.rb +0 -15
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "contexts/action_builder"
|
|
4
|
+
require_relative "contexts/element_builder"
|
|
5
|
+
|
|
6
|
+
module Tailmix
|
|
7
|
+
module Definition
|
|
8
|
+
class ContextBuilder
|
|
9
|
+
attr_reader :elements, :actions
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@elements = {}
|
|
13
|
+
@actions = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def element(name, base_classes = "", &block)
|
|
17
|
+
builder = Contexts::ElementBuilder.new(name)
|
|
18
|
+
builder.attributes.classes(base_classes.split)
|
|
19
|
+
|
|
20
|
+
builder.instance_eval(&block) if block
|
|
21
|
+
|
|
22
|
+
@elements[name.to_sym] = builder
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def action(name, method:, &block)
|
|
26
|
+
builder = Contexts::ActionBuilder.new(method)
|
|
27
|
+
builder.instance_eval(&block) if block
|
|
28
|
+
@actions[name.to_sym] = builder
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_definition
|
|
32
|
+
Definition::Result::Context.new(
|
|
33
|
+
elements: @elements.transform_values(&:build_definition).freeze,
|
|
34
|
+
actions: @actions.transform_values(&:build_definition).freeze
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "actions/element_builder"
|
|
4
|
+
|
|
5
|
+
module Tailmix
|
|
6
|
+
module Definition
|
|
7
|
+
module Contexts
|
|
8
|
+
class ActionBuilder
|
|
9
|
+
def initialize(method)
|
|
10
|
+
@method = method
|
|
11
|
+
@mutations = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def element(name, &block)
|
|
15
|
+
builder = Actions::ElementBuilder.new(@method)
|
|
16
|
+
builder.instance_eval(&block)
|
|
17
|
+
|
|
18
|
+
commands = builder.build_commands
|
|
19
|
+
@mutations[name.to_sym] = commands unless commands.empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_definition
|
|
23
|
+
Definition::Result::Action.new(
|
|
24
|
+
action: @method,
|
|
25
|
+
mutations: @mutations.freeze
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Definition
|
|
5
|
+
module Contexts
|
|
6
|
+
module Actions
|
|
7
|
+
class ElementBuilder
|
|
8
|
+
def initialize(default_method)
|
|
9
|
+
@default_method = default_method
|
|
10
|
+
@commands = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def classes(classes_string, method: @default_method)
|
|
14
|
+
@commands << { field: :classes, method: method, payload: classes_string }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def data(data_hash)
|
|
18
|
+
operation = data_hash.delete(:method) || @default_method
|
|
19
|
+
|
|
20
|
+
@commands << { field: :data, method: operation, payload: data_hash }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build_commands
|
|
24
|
+
@commands
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Definition
|
|
5
|
+
module Contexts
|
|
6
|
+
class AttributeBuilder
|
|
7
|
+
def initialize
|
|
8
|
+
@classes = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def classes(*list)
|
|
12
|
+
@classes.concat(list.flatten.map(&:to_s))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_definition
|
|
16
|
+
Definition::Result::Attributes.new(classes: @classes.freeze)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DimensionBuilder
|
|
4
|
+
attr_reader :options
|
|
5
|
+
|
|
6
|
+
def initialize(default: nil)
|
|
7
|
+
@options = { options: {}, default: default }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def option(value, classes, default: false)
|
|
11
|
+
@options[:options][value] = classes.split
|
|
12
|
+
if default && @options[:default].nil?
|
|
13
|
+
@options[:default] = value
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "attribute_builder"
|
|
4
|
+
require_relative "stimulus_builder"
|
|
5
|
+
require_relative "dimension_builder"
|
|
6
|
+
|
|
7
|
+
module Tailmix
|
|
8
|
+
module Definition
|
|
9
|
+
module Contexts
|
|
10
|
+
class ElementBuilder
|
|
11
|
+
def initialize(name)
|
|
12
|
+
@name = name
|
|
13
|
+
@dimensions = {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def attributes
|
|
17
|
+
@attributes_builder ||= AttributeBuilder.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stimulus
|
|
21
|
+
@stimulus_builder ||= StimulusBuilder.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def dimension(name, default: nil, &block)
|
|
25
|
+
dimension = DimensionBuilder.new(default: default)
|
|
26
|
+
dimension.instance_eval(&block)
|
|
27
|
+
@dimensions[name.to_sym] = dimension.options
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def build_definition
|
|
31
|
+
Definition::Result::Element.new(
|
|
32
|
+
name: @name,
|
|
33
|
+
attributes: attributes.build_definition,
|
|
34
|
+
stimulus: stimulus.build_definition,
|
|
35
|
+
dimensions: @dimensions.freeze
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Definition
|
|
5
|
+
module Contexts
|
|
6
|
+
class StimulusBuilder
|
|
7
|
+
attr_reader :definitions
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@definitions = []
|
|
11
|
+
@current_context = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def controller(name)
|
|
15
|
+
@definitions << { type: :controller, name: name }
|
|
16
|
+
context(name)
|
|
17
|
+
end
|
|
18
|
+
alias_method :ctr, :controller
|
|
19
|
+
|
|
20
|
+
def context(name)
|
|
21
|
+
@current_context = name.to_s
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
alias_method :ctx, :context
|
|
25
|
+
|
|
26
|
+
def target(target_name)
|
|
27
|
+
raise "A controller context must be set..." unless @current_context
|
|
28
|
+
@definitions << { type: :target, controller: @current_context, name: target_name }
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def action(*args)
|
|
33
|
+
ensure_context!
|
|
34
|
+
|
|
35
|
+
action_data = case args.first
|
|
36
|
+
when String
|
|
37
|
+
{ type: :raw, content: args.first }
|
|
38
|
+
when Hash
|
|
39
|
+
{ type: :hash, content: args.first }
|
|
40
|
+
else
|
|
41
|
+
{ type: :tuple, content: args }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@definitions << { type: :action, controller: @current_context, data: action_data }
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def value(value_name, value: nil, call: nil, method: nil)
|
|
49
|
+
ensure_context!
|
|
50
|
+
|
|
51
|
+
source = if !value.nil?
|
|
52
|
+
{ type: :literal, content: value }
|
|
53
|
+
elsif call.is_a?(Proc)
|
|
54
|
+
{ type: :proc, content: call }
|
|
55
|
+
elsif method.is_a?(Symbol) || method.is_a?(String)
|
|
56
|
+
{ type: :method, content: method.to_sym }
|
|
57
|
+
else
|
|
58
|
+
raise ArgumentError, "You must provide one of value:, call:, or method: keyword arguments."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@definitions << {
|
|
62
|
+
type: :value,
|
|
63
|
+
controller: @current_context,
|
|
64
|
+
name: value_name,
|
|
65
|
+
source: source
|
|
66
|
+
}
|
|
67
|
+
self
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def action_payload(action_name, as: nil)
|
|
71
|
+
ensure_context!
|
|
72
|
+
value_name = as || "#{action_name}_action"
|
|
73
|
+
|
|
74
|
+
@definitions << {
|
|
75
|
+
type: :action_payload,
|
|
76
|
+
controller: @current_context,
|
|
77
|
+
action_name: action_name.to_sym,
|
|
78
|
+
value_name: value_name.to_sym
|
|
79
|
+
}
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def param(params_hash)
|
|
84
|
+
ensure_context!
|
|
85
|
+
@definitions << { type: :param, controller: @current_context, params: params_hash }
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_definition
|
|
90
|
+
Definition::Result::Stimulus.new(definitions: @definitions.freeze)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def ensure_context!
|
|
96
|
+
raise "A controller context must be set via .controller() or .context() before this call." unless @current_context
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Definition
|
|
5
|
+
# A service object responsible for deep-merging two tailmix definitions.
|
|
6
|
+
class Merger
|
|
7
|
+
def self.call(parent_def, child_def)
|
|
8
|
+
new(parent_def, child_def).merge
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(parent_def, child_def)
|
|
12
|
+
@parent_def = parent_def
|
|
13
|
+
@child_def = child_def
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def merge
|
|
17
|
+
Result::Context.new(
|
|
18
|
+
elements: merged_elements,
|
|
19
|
+
actions: merged_actions
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def merged_actions
|
|
26
|
+
# For actions, child definitions completely override parent definitions.
|
|
27
|
+
@parent_def.actions.merge(@child_def.actions)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def merged_elements
|
|
31
|
+
all_element_keys = (@parent_def.elements.keys | @child_def.elements.keys)
|
|
32
|
+
|
|
33
|
+
all_element_keys.each_with_object({}) do |key, h|
|
|
34
|
+
parent_element = @parent_def.elements[key]
|
|
35
|
+
child_element = @child_def.elements[key]
|
|
36
|
+
|
|
37
|
+
h[key] = if parent_element && child_element
|
|
38
|
+
merge_element(parent_element, child_element)
|
|
39
|
+
else
|
|
40
|
+
child_element || parent_element
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def merge_element(parent_el, child_el)
|
|
46
|
+
Result::Element.new(
|
|
47
|
+
name: parent_el.name,
|
|
48
|
+
attributes: merge_attributes(parent_el.attributes, child_el.attributes),
|
|
49
|
+
dimensions: merge_dimensions(parent_el.dimensions, child_el.dimensions),
|
|
50
|
+
stimulus: merge_stimulus(parent_el.stimulus, child_el.stimulus)
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def merge_attributes(parent_attrs, child_attrs)
|
|
55
|
+
# Combine base classes, ensuring no duplicates.
|
|
56
|
+
combined_classes = (parent_attrs.classes + child_attrs.classes).uniq
|
|
57
|
+
Result::Attributes.new(classes: combined_classes)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def merge_stimulus(parent_stimulus, child_stimulus)
|
|
61
|
+
# Combine stimulus definitions.
|
|
62
|
+
combined_definitions = parent_stimulus.definitions + child_stimulus.definitions
|
|
63
|
+
Result::Stimulus.new(definitions: combined_definitions)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def merge_dimensions(parent_dims, child_dims)
|
|
69
|
+
deep_merge(parent_dims, child_dims) do |key, parent_val, child_val|
|
|
70
|
+
if key == :options && parent_val.is_a?(Hash) && child_val.is_a?(Hash)
|
|
71
|
+
parent_val.merge(child_val)
|
|
72
|
+
else
|
|
73
|
+
child_val
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def deep_merge(parent_hash, child_hash, &block)
|
|
79
|
+
child_hash.each_with_object(parent_hash.dup) do |(key, child_val), new_hash|
|
|
80
|
+
parent_val = new_hash[key]
|
|
81
|
+
|
|
82
|
+
new_hash[key] = if parent_val.is_a?(Hash) && child_val.is_a?(Hash)
|
|
83
|
+
deep_merge(parent_val, child_val, &block)
|
|
84
|
+
elsif block_given? && new_hash.key?(key)
|
|
85
|
+
block.call(key, parent_val, child_val)
|
|
86
|
+
else
|
|
87
|
+
child_val
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Definition
|
|
5
|
+
module Result
|
|
6
|
+
Context = Struct.new(:elements, :actions, keyword_init: true) do
|
|
7
|
+
def to_h
|
|
8
|
+
{
|
|
9
|
+
elements: elements.transform_values(&:to_h),
|
|
10
|
+
actions: actions.transform_values(&:to_h)
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Element = Struct.new(:name, :attributes, :dimensions, :stimulus, keyword_init: true) do
|
|
16
|
+
def to_h
|
|
17
|
+
{
|
|
18
|
+
name: name,
|
|
19
|
+
attributes: attributes.to_h,
|
|
20
|
+
dimensions: dimensions,
|
|
21
|
+
stimulus: stimulus.to_h
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
Attributes = Struct.new(:classes, keyword_init: true)
|
|
27
|
+
Stimulus = Struct.new(:definitions, keyword_init: true)
|
|
28
|
+
Action = Struct.new(:action, :mutations, keyword_init: true)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Dev
|
|
5
|
+
class Docs
|
|
6
|
+
|
|
7
|
+
def initialize(tools)
|
|
8
|
+
@tools = tools
|
|
9
|
+
@definition = tools.definition
|
|
10
|
+
@component_class_name = tools.component_class
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate
|
|
14
|
+
output = ["== Tailmix Docs for #{@component_class_name} =="]
|
|
15
|
+
|
|
16
|
+
signature = generate_signature
|
|
17
|
+
output << "Signature: `initialize(#{signature})`" unless signature.empty?
|
|
18
|
+
output << ""
|
|
19
|
+
|
|
20
|
+
output << generate_dimensions_docs
|
|
21
|
+
output << ""
|
|
22
|
+
output << generate_actions_docs
|
|
23
|
+
output << ""
|
|
24
|
+
output << generate_stimulus_docs
|
|
25
|
+
|
|
26
|
+
output.join("\n")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def generate_signature
|
|
32
|
+
all_dimensions
|
|
33
|
+
.map { |name, config| "#{name}: #{config[:default].inspect}" if config.key?(:default) }
|
|
34
|
+
.compact
|
|
35
|
+
.join(", ")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def generate_dimensions_docs
|
|
39
|
+
output = []
|
|
40
|
+
|
|
41
|
+
if all_dimensions.any?
|
|
42
|
+
output << "Dimensions:"
|
|
43
|
+
all_dimensions.each do |dim_name, config|
|
|
44
|
+
default_info = config[:default] ? "(default: #{config[:default].inspect})" : ""
|
|
45
|
+
output << " - #{dim_name} #{default_info}"
|
|
46
|
+
config[:options].each do |option_key, option_value|
|
|
47
|
+
output << " - #{option_key.inspect}: \"#{option_value.join(' ')}\""
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
output << "No dimensions defined."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
output.join("\n")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def generate_actions_docs
|
|
58
|
+
output = []
|
|
59
|
+
actions = @definition.actions
|
|
60
|
+
|
|
61
|
+
if actions.any?
|
|
62
|
+
output << "Actions:"
|
|
63
|
+
actions.keys.each do |action_name|
|
|
64
|
+
output << " - :#{action_name}"
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
output << "No actions defined."
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
output.join("\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def generate_stimulus_docs
|
|
74
|
+
@tools.stimulus.scaffold(show_docs: true)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def all_dimensions
|
|
78
|
+
@_all_dimensions ||= @definition.elements.values.flat_map(&:dimensions).reduce({}, :merge)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Dev
|
|
5
|
+
class StimulusGenerator
|
|
6
|
+
def initialize(definition, component_name)
|
|
7
|
+
@definition = definition
|
|
8
|
+
@component_name = component_name
|
|
9
|
+
@stimulus_defs = definition.elements.values.flat_map(&:stimulus).flat_map(&:definitions)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def scaffold(controller_name = nil, show_docs: false)
|
|
13
|
+
controllers_to_generate = controller_name ? [ controller_name.to_s ] : all_controllers
|
|
14
|
+
|
|
15
|
+
output = controllers_to_generate.map do |name|
|
|
16
|
+
defs = @stimulus_defs.select { |d| d[:controller] == name }
|
|
17
|
+
|
|
18
|
+
show_docs ? generate_docs_for(name, defs) + "\n" : generate_js_for(name, defs) + ("-" * 60) + "\n"
|
|
19
|
+
end
|
|
20
|
+
output.join("\n")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def generate_docs_for(controller_name, defs)
|
|
26
|
+
output = [ "Stimulus:" ]
|
|
27
|
+
output << " - on `#{controller_name}` controller:"
|
|
28
|
+
targets = defs.select { |d| d[:type] == :target }.map { |d| d[:name] }
|
|
29
|
+
output << " - Targets: #{targets.join(', ')}" if targets.any?
|
|
30
|
+
|
|
31
|
+
actions = action_methods(defs)
|
|
32
|
+
output << " - Actions: #{actions.join(', ')}" if actions.any?
|
|
33
|
+
|
|
34
|
+
values = defs.select { |d| d[:type] == :value }.map { |d| d[:name] }
|
|
35
|
+
output << " - Values: #{values.join(', ')}" if values.any?
|
|
36
|
+
|
|
37
|
+
output.join("\n")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def generate_js_for(controller_name, defs)
|
|
41
|
+
targets = defs.select { |d| d[:type] == :target }.map { |d| "'#{d[:name]}'" }.uniq.join(", ")
|
|
42
|
+
|
|
43
|
+
payload_actions = defs.select { |d| d[:type] == :action_payload }
|
|
44
|
+
simple_values = defs.select { |d| d[:type] == :value }
|
|
45
|
+
|
|
46
|
+
value_names = (payload_actions.map { |d| d[:value_name] } + simple_values.map { |d| d[:name] })
|
|
47
|
+
.uniq.map { |name| "#{snake_to_camel(name.to_s)}: Object" }.join(", ")
|
|
48
|
+
|
|
49
|
+
isomorphic_methods = payload_actions.map do |payload_def|
|
|
50
|
+
action_name_camel = snake_to_camel(payload_def[:action_name].to_s)
|
|
51
|
+
value_name_camel = snake_to_camel(payload_def[:value_name].to_s)
|
|
52
|
+
|
|
53
|
+
" #{action_name_camel}(event) {\n if (event) event.preventDefault();\n Tailmix.run({ config: this.#{value_name_camel}Value, controller: this });\n }"
|
|
54
|
+
end.join.strip
|
|
55
|
+
|
|
56
|
+
standard_action_names = defs.select { |d| d[:type] == :action }
|
|
57
|
+
.flat_map { |d| extract_action_methods(d[:data]) }
|
|
58
|
+
.uniq
|
|
59
|
+
|
|
60
|
+
implemented_action_names = payload_actions.map { |d| d[:action_name].to_s }
|
|
61
|
+
stub_methods = (standard_action_names - implemented_action_names).map do |method_name|
|
|
62
|
+
method_name_camel = snake_to_camel(method_name)
|
|
63
|
+
" #{method_name_camel}() {\n console.log('#{controller_name}##{method_name_camel} fired');\n }"
|
|
64
|
+
end.join("\n\n")
|
|
65
|
+
|
|
66
|
+
js_methods = [isomorphic_methods, stub_methods].reject(&:empty?).join("\n\n")
|
|
67
|
+
|
|
68
|
+
<<~JAVASCRIPT
|
|
69
|
+
// Generated by Tailmix for the "#{controller_name}" controller
|
|
70
|
+
// Path: app/javascript/controllers/#{controller_name.tr('_', '-')}_controller.js
|
|
71
|
+
import { Controller } from "@hotwired/stimulus"
|
|
72
|
+
import Tailmix from "tailmix"
|
|
73
|
+
|
|
74
|
+
export default class extends Controller {
|
|
75
|
+
static targets = [#{targets}]
|
|
76
|
+
static values = { #{value_names} }
|
|
77
|
+
|
|
78
|
+
connect() {
|
|
79
|
+
console.log("#{controller_name} controller connected to", this.element);
|
|
80
|
+
}
|
|
81
|
+
#{js_methods}
|
|
82
|
+
}
|
|
83
|
+
JAVASCRIPT
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def extract_action_methods(action_data)
|
|
87
|
+
case action_data[:type]
|
|
88
|
+
when :raw
|
|
89
|
+
action_data[:content].to_s.scan(/#(\w+)/).flatten
|
|
90
|
+
when :hash
|
|
91
|
+
action_data[:content].values.map(&:to_s)
|
|
92
|
+
when :tuple
|
|
93
|
+
[action_data[:content][1].to_s]
|
|
94
|
+
else
|
|
95
|
+
[]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def all_controllers
|
|
100
|
+
@stimulus_defs.map { |d| d[:controller] }.compact.uniq
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def action_methods(defs)
|
|
104
|
+
defs.select { |d| d[:type] == :action }.flat_map do |action_definition|
|
|
105
|
+
data = action_definition[:data]
|
|
106
|
+
case data[:type]
|
|
107
|
+
when :raw
|
|
108
|
+
data[:content].to_s.scan(/#(\w+)/).flatten
|
|
109
|
+
when :hash
|
|
110
|
+
data[:content].values.map(&:to_s)
|
|
111
|
+
when :tuple
|
|
112
|
+
[ data[:content][1].to_s ]
|
|
113
|
+
else
|
|
114
|
+
[]
|
|
115
|
+
end
|
|
116
|
+
end.uniq
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def snake_to_camel(str)
|
|
120
|
+
str.split("_").map.with_index { |word, i| i.zero? ? word : word.capitalize }.join
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "stimulus_generator"
|
|
4
|
+
require_relative "docs"
|
|
5
|
+
|
|
6
|
+
module Tailmix
|
|
7
|
+
module Dev
|
|
8
|
+
class Tools
|
|
9
|
+
attr_reader :definition, :component_class
|
|
10
|
+
|
|
11
|
+
def initialize(component_class)
|
|
12
|
+
@component_class = component_class
|
|
13
|
+
@definition = component_class.tailmix_definition
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def docs
|
|
17
|
+
Dev::Docs.new(self).generate
|
|
18
|
+
end
|
|
19
|
+
alias_method :help, :docs
|
|
20
|
+
|
|
21
|
+
def stimulus
|
|
22
|
+
StimulusGenerator.new(@definition, @component_class.name)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
config.before_initialize do
|
|
6
|
+
Rails.application.config.assets.paths << Engine.root.join("app/javascript")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
PRECOMPILE_ASSETS = %w[ index.js runner.js finder.js mutator.js stimulus_adapter.js ]
|
|
10
|
+
|
|
11
|
+
initializer "tailmix.assets" do
|
|
12
|
+
if Rails.application.config.respond_to?(:assets)
|
|
13
|
+
Rails.application.config.assets.precompile += PRECOMPILE_ASSETS
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|