tailmix 0.4.7 → 0.4.8
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/CHANGELOG.md +8 -0
- data/README.md +46 -205
- data/app/javascript/tailmix/runtime/action_dispatcher.js +132 -0
- data/app/javascript/tailmix/runtime/component.js +130 -0
- data/app/javascript/tailmix/runtime/index.js +130 -0
- data/app/javascript/tailmix/runtime/plugins.js +35 -0
- data/app/javascript/tailmix/runtime/updater.js +140 -0
- data/docs/01_getting_started.md +0 -81
- data/examples/_modal_component.arb +17 -25
- data/examples/modal_component.rb +31 -155
- data/lib/tailmix/definition/builders/action_builder.rb +39 -0
- data/lib/tailmix/definition/{contexts → builders}/actions/element_builder.rb +1 -1
- data/lib/tailmix/definition/{contexts → builders}/attribute_builder.rb +1 -1
- data/lib/tailmix/definition/builders/component_builder.rb +81 -0
- data/lib/tailmix/definition/{contexts → builders}/dimension_builder.rb +2 -2
- data/lib/tailmix/definition/builders/element_builder.rb +83 -0
- data/lib/tailmix/definition/builders/reactor_builder.rb +43 -0
- data/lib/tailmix/definition/builders/rule_builder.rb +58 -0
- data/lib/tailmix/definition/builders/state_builder.rb +21 -0
- data/lib/tailmix/definition/{contexts → builders}/variant_builder.rb +17 -2
- data/lib/tailmix/definition/context_builder.rb +17 -12
- data/lib/tailmix/definition/payload_proxy.rb +16 -0
- data/lib/tailmix/definition/result.rb +17 -19
- data/lib/tailmix/dev/docs.rb +3 -9
- data/lib/tailmix/dev/tools.rb +0 -5
- data/lib/tailmix/dsl.rb +3 -5
- data/lib/tailmix/engine.rb +11 -1
- data/lib/tailmix/html/attributes.rb +22 -12
- data/lib/tailmix/middleware/registry_cleaner.rb +17 -0
- data/lib/tailmix/registry.rb +37 -0
- data/lib/tailmix/runtime/action_proxy.rb +29 -0
- data/lib/tailmix/runtime/attribute_builder.rb +102 -0
- data/lib/tailmix/runtime/attribute_cache.rb +23 -0
- data/lib/tailmix/runtime/context.rb +51 -62
- data/lib/tailmix/runtime/facade_builder.rb +8 -5
- data/lib/tailmix/runtime/state.rb +36 -0
- data/lib/tailmix/runtime/state_proxy.rb +34 -0
- data/lib/tailmix/runtime.rb +0 -1
- data/lib/tailmix/version.rb +1 -1
- data/lib/tailmix/view_helpers.rb +49 -0
- data/lib/tailmix.rb +4 -2
- metadata +26 -20
- data/app/javascript/tailmix/finder.js +0 -15
- data/app/javascript/tailmix/index.js +0 -7
- data/app/javascript/tailmix/mutator.js +0 -28
- data/app/javascript/tailmix/runner.js +0 -7
- data/app/javascript/tailmix/stimulus_adapter.js +0 -37
- data/docs/02_dsl_reference.md +0 -266
- data/docs/03_advanced_usage.md +0 -88
- data/docs/04_client_side_bridge.md +0 -119
- data/docs/05_cookbook.md +0 -249
- data/lib/tailmix/definition/contexts/action_builder.rb +0 -31
- data/lib/tailmix/definition/contexts/element_builder.rb +0 -55
- data/lib/tailmix/definition/contexts/stimulus_builder.rb +0 -101
- data/lib/tailmix/dev/stimulus_generator.rb +0 -124
- data/lib/tailmix/runtime/stimulus/compiler.rb +0 -59
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
require "erb"
|
|
4
4
|
require_relative "class_list"
|
|
5
5
|
require_relative "data_map"
|
|
6
|
-
require_relative "selector"
|
|
7
6
|
|
|
8
7
|
module Tailmix
|
|
9
8
|
module HTML
|
|
10
9
|
class Attributes < Hash
|
|
11
|
-
attr_reader :element_name
|
|
10
|
+
attr_reader :element_name
|
|
12
11
|
|
|
13
|
-
def initialize(initial_hash = {}, element_name: nil,
|
|
12
|
+
def initialize(initial_hash = {}, element_name: nil, context: nil)
|
|
14
13
|
@element_name = element_name
|
|
15
|
-
@
|
|
14
|
+
@context = context
|
|
16
15
|
super()
|
|
17
16
|
|
|
18
17
|
attrs_to_merge = initial_hash.dup
|
|
@@ -24,24 +23,27 @@ module Tailmix
|
|
|
24
23
|
self[:class] = ClassList.new(initial_classes)
|
|
25
24
|
self[:data] = DataMap.new("data", initial_data || {})
|
|
26
25
|
self[:aria] = DataMap.new("aria", initial_aria || {})
|
|
27
|
-
self[:tailmix] = Selector.new(element_name, variant_string)
|
|
28
26
|
|
|
29
27
|
merge!(attrs_to_merge)
|
|
30
28
|
end
|
|
31
29
|
|
|
30
|
+
|
|
32
31
|
def each(&block)
|
|
33
32
|
to_h.each(&block)
|
|
34
33
|
end
|
|
34
|
+
alias_method :each_pair, :each
|
|
35
35
|
|
|
36
36
|
def to_h
|
|
37
|
-
final_attrs = select { |k, _| !%i[class data aria
|
|
37
|
+
final_attrs = select { |k, _| !%i[class data aria].include?(k.to_sym) }
|
|
38
38
|
|
|
39
39
|
class_string = self[:class].to_s
|
|
40
40
|
final_attrs[:class] = class_string unless class_string.empty?
|
|
41
41
|
|
|
42
42
|
final_attrs.merge!(self[:data].to_h)
|
|
43
43
|
final_attrs.merge!(self[:aria].to_h)
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
final_attrs["data-tailmix-element"] = @element_name if @element_name
|
|
46
|
+
final_attrs["data-tailmix-id"] = @context.id if @context.id
|
|
45
47
|
|
|
46
48
|
final_attrs
|
|
47
49
|
end
|
|
@@ -67,10 +69,6 @@ module Tailmix
|
|
|
67
69
|
data.stimulus
|
|
68
70
|
end
|
|
69
71
|
|
|
70
|
-
def tailmix
|
|
71
|
-
self[:tailmix]
|
|
72
|
-
end
|
|
73
|
-
|
|
74
72
|
def toggle(class_names)
|
|
75
73
|
classes.toggle(class_names)
|
|
76
74
|
self
|
|
@@ -87,8 +85,20 @@ module Tailmix
|
|
|
87
85
|
end
|
|
88
86
|
|
|
89
87
|
def each_attribute(&block)
|
|
90
|
-
[ classes: classes, data: data.to_h, aria: aria.to_h
|
|
88
|
+
[ classes: classes, data: data.to_h, aria: aria.to_h ].each(&block)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def component
|
|
92
|
+
raise "No context available to build component root" unless @context
|
|
93
|
+
|
|
94
|
+
root_attrs = {
|
|
95
|
+
"data-tailmix-component" => @context.component_name,
|
|
96
|
+
"data-tailmix-state" => @context.state_payload,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
self.class.new(self.to_h.merge(root_attrs), element_name: @element_name, context: @context)
|
|
91
100
|
end
|
|
101
|
+
alias_method :root, :component
|
|
92
102
|
end
|
|
93
103
|
end
|
|
94
104
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "singleton"
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Tailmix
|
|
6
|
+
# A per-request registry to store unique component classes rendered
|
|
7
|
+
# during a request-response cycle.
|
|
8
|
+
class Registry
|
|
9
|
+
include Singleton
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@component_classes = Set.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Registers a component class.
|
|
16
|
+
# @param component_class [Class] The component class to register.
|
|
17
|
+
def register(component_class)
|
|
18
|
+
@component_classes.add(component_class)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Gathers definitions from all registered classes.
|
|
22
|
+
# @return [Hash] A hash mapping component names to their definitions.
|
|
23
|
+
def definitions
|
|
24
|
+
@component_classes.each_with_object({}) do |klass, hash|
|
|
25
|
+
component_name = klass.name
|
|
26
|
+
if component_name && klass.respond_to?(:tailmix_definition)
|
|
27
|
+
hash[component_name] = klass.tailmix_definition.to_h
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Clears the registry. Must be called after each request.
|
|
33
|
+
def clear!
|
|
34
|
+
@component_classes.clear
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Runtime
|
|
5
|
+
# Proxy for convenient calling of actions (ui.action).
|
|
6
|
+
class ActionProxy
|
|
7
|
+
def initialize(context)
|
|
8
|
+
@context = context
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def method_missing(method_name, *args, &block)
|
|
12
|
+
action_name = method_name.to_sym
|
|
13
|
+
action_def = @context.definition.actions[action_name]
|
|
14
|
+
|
|
15
|
+
unless action_def
|
|
16
|
+
raise NoMethodError, "undefined action `#{action_name}` for #{@context.component_name}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# We return an object that can be called using .call.
|
|
20
|
+
# This allows you to write ui.action.save.call(payload)
|
|
21
|
+
->(payload = {}) { @context.run_action(action_def, payload) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
25
|
+
@context.definition.actions.key?(method_name.to_sym) || super
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Runtime
|
|
5
|
+
class AttributeBuilder
|
|
6
|
+
def initialize(element_def, state, context)
|
|
7
|
+
@element_def = element_def
|
|
8
|
+
@state = state
|
|
9
|
+
@context = context
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build
|
|
13
|
+
attributes = create_base_attributes
|
|
14
|
+
|
|
15
|
+
apply_dimensions(attributes)
|
|
16
|
+
apply_compound_variants(attributes)
|
|
17
|
+
apply_attribute_bindings(attributes)
|
|
18
|
+
apply_model_bindings(attributes)
|
|
19
|
+
apply_event_bindings(attributes)
|
|
20
|
+
|
|
21
|
+
attributes
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def create_base_attributes
|
|
27
|
+
base_attrs = @element_def.default_attributes.merge(
|
|
28
|
+
class: @element_def.attributes.classes
|
|
29
|
+
)
|
|
30
|
+
HTML::Attributes.new(base_attrs, element_name: @element_def.name, context: @context)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Applies classes and data/aria attributes from `dimension`.
|
|
34
|
+
def apply_dimensions(attributes)
|
|
35
|
+
@element_def.dimensions.each do |name, dim_def|
|
|
36
|
+
value = @state[name] || dim_def[:default]
|
|
37
|
+
next if value.nil?
|
|
38
|
+
|
|
39
|
+
variant_def = dim_def.fetch(:variants, {}).fetch(value, nil)
|
|
40
|
+
|
|
41
|
+
next unless variant_def
|
|
42
|
+
attributes.classes.add(variant_def.classes)
|
|
43
|
+
attributes.data.merge!(variant_def.data)
|
|
44
|
+
attributes.aria.merge!(variant_def.aria)
|
|
45
|
+
attributes.merge!(variant_def.attributes)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Applies classes and data/aria attributes from `compound_variant`.
|
|
50
|
+
def apply_compound_variants(attributes)
|
|
51
|
+
@element_def.compound_variants.each do |cv|
|
|
52
|
+
next unless cv[:on].all? { |key, value| @state[key] == value }
|
|
53
|
+
|
|
54
|
+
modifications = cv[:modifications]
|
|
55
|
+
attributes.classes.add(modifications.classes)
|
|
56
|
+
attributes.data.merge!(modifications.data) if modifications.data
|
|
57
|
+
attributes.aria.merge!(modifications.aria) if modifications.aria
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Applies one-way attribute bindings (`bind :src, to: :url`).
|
|
62
|
+
def apply_attribute_bindings(attributes)
|
|
63
|
+
@element_def.attribute_bindings&.each do |attr_name, state_key_or_proc|
|
|
64
|
+
next if %i[text html].include?(attr_name)
|
|
65
|
+
|
|
66
|
+
value = if state_key_or_proc.is_a?(Proc)
|
|
67
|
+
state_key_or_proc.call(@state)
|
|
68
|
+
else
|
|
69
|
+
@state[state_key_or_proc]
|
|
70
|
+
end
|
|
71
|
+
attributes[attr_name] = value if value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Applies two-way bindings (`model :value, to: :query`).
|
|
76
|
+
def apply_model_bindings(attributes)
|
|
77
|
+
@element_def.model_bindings&.each do |attr_name, binding_def|
|
|
78
|
+
state_key = binding_def[:state]
|
|
79
|
+
value = @state[state_key]
|
|
80
|
+
attributes[attr_name] = value if value
|
|
81
|
+
|
|
82
|
+
# We are adding data attributes that will "bring to life" the client-side JS.
|
|
83
|
+
attributes.data.add("tailmix-model-attr": attr_name)
|
|
84
|
+
attributes.data.add("tailmix-model-state": state_key)
|
|
85
|
+
attributes.data.add("tailmix-model-event": binding_def[:event])
|
|
86
|
+
attributes.data.add("tailmix-model-action": binding_def[:action]) if binding_def[:action]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Applies event handlers (`on :click, :save`).
|
|
91
|
+
def apply_event_bindings(attributes)
|
|
92
|
+
return unless @element_def.event_bindings&.any?
|
|
93
|
+
|
|
94
|
+
action_string = @element_def.event_bindings.map { |b| "#{b[:event]}->#{b[:action]}" }.join(" ")
|
|
95
|
+
with_map = @element_def.event_bindings.map { |b| b[:with] }.compact.reduce({}, :merge)
|
|
96
|
+
|
|
97
|
+
attributes.data.add(tailmix_action: action_string)
|
|
98
|
+
attributes.data.add(tailmix_action_with: with_map.to_json) unless with_map.empty?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Runtime
|
|
5
|
+
class AttributeCache
|
|
6
|
+
def initialize
|
|
7
|
+
@cache = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def get(element_name)
|
|
11
|
+
@cache[element_name.to_sym]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def set(element_name, attributes)
|
|
15
|
+
@cache[element_name.to_sym] = attributes
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def clear!
|
|
19
|
+
@cache.clear
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -1,88 +1,77 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "../registry"
|
|
4
|
+
require_relative "state"
|
|
5
|
+
require_relative "state_proxy"
|
|
6
|
+
require_relative "action_proxy"
|
|
7
|
+
require_relative "attribute_cache"
|
|
8
|
+
require_relative "attribute_builder"
|
|
2
9
|
|
|
3
10
|
module Tailmix
|
|
4
11
|
module Runtime
|
|
5
12
|
class Context
|
|
6
|
-
attr_reader :component_instance, :definition, :
|
|
13
|
+
attr_reader :component_instance, :definition, :id, :state
|
|
7
14
|
|
|
8
|
-
def initialize(component_instance, definition,
|
|
15
|
+
def initialize(component_instance, definition, initial_state = {}, id: nil)
|
|
9
16
|
@component_instance = component_instance
|
|
10
17
|
@definition = definition
|
|
11
|
-
@
|
|
12
|
-
@attributes_cache = {}
|
|
13
|
-
end
|
|
18
|
+
@id = id
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@attributes_cache = source.instance_variable_get(:@attributes_cache).transform_values(&:dup)
|
|
18
|
-
end
|
|
20
|
+
@cache = AttributeCache.new
|
|
21
|
+
@state = State.new(definition.states, initial_state, cache: @cache)
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
@attributes_cache[element_name] ||= build_attributes_for(element_name, @dimensions)
|
|
23
|
+
Registry.instance.register(component_instance.class)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
def state_proxy
|
|
27
|
+
@state_proxy ||= StateProxy.new(self)
|
|
28
|
+
end
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
@
|
|
30
|
-
attributes_object
|
|
30
|
+
def action_proxy
|
|
31
|
+
@action_proxy ||= ActionProxy.new(self)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
def
|
|
34
|
-
|
|
34
|
+
def run_action(action_def, payload)
|
|
35
|
+
action_def.transitions.each do |transition|
|
|
36
|
+
# Here we simulate what JS does on the client.
|
|
37
|
+
case transition[:type]
|
|
38
|
+
when :set
|
|
39
|
+
# Processing PayloadProxy, if it exists.
|
|
40
|
+
value = transition[:payload][:value]
|
|
41
|
+
if value.is_a?(Hash) && value[:__type] == 'payload_value'
|
|
42
|
+
set_state(transition[:payload][:key], payload[value[:key]])
|
|
43
|
+
else
|
|
44
|
+
set_state(transition[:payload][:key], value)
|
|
45
|
+
end
|
|
46
|
+
when :toggle
|
|
47
|
+
key = transition[:payload][:key]
|
|
48
|
+
set_state(key, !get_state(key))
|
|
49
|
+
# `refresh` and `dispatch` are purely client-side operations; we ignore them on the server.
|
|
50
|
+
end
|
|
51
|
+
end
|
|
35
52
|
end
|
|
36
53
|
|
|
37
|
-
|
|
54
|
+
def attributes_for(element_name)
|
|
55
|
+
cached = @cache.get(element_name)
|
|
56
|
+
return cached if cached
|
|
38
57
|
|
|
39
|
-
def build_attributes_for(element_name, dimensions)
|
|
40
58
|
element_def = @definition.elements.fetch(element_name)
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{ class: element_def.attributes.classes },
|
|
47
|
-
element_name: element_def.name,
|
|
48
|
-
variant_string: variant_string,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
element_def.dimensions.each do |name, dim_def|
|
|
52
|
-
value = dimensions.fetch(name, dim_def[:default])
|
|
53
|
-
next if value.nil?
|
|
54
|
-
|
|
55
|
-
variant_def = dim_def.fetch(:variants, {}).fetch(value, nil)
|
|
56
|
-
next unless variant_def
|
|
57
|
-
|
|
58
|
-
attributes.classes.add(variant_def.classes)
|
|
59
|
-
attributes.data.merge!(variant_def.data)
|
|
60
|
-
attributes.aria.merge!(variant_def.aria)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
element_def.compound_variants.each do |cv|
|
|
64
|
-
conditions = cv[:on]
|
|
65
|
-
modifications = cv[:modifications]
|
|
66
|
-
|
|
67
|
-
match = conditions.all? do |key, value|
|
|
68
|
-
dimensions[key] == value
|
|
69
|
-
end
|
|
60
|
+
attributes = AttributeBuilder.new(element_def, @state, self).build
|
|
61
|
+
@cache.set(element_name, attributes)
|
|
62
|
+
attributes
|
|
63
|
+
end
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
attributes.aria.merge!(modifications.aria)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
65
|
+
def component_name
|
|
66
|
+
@component_instance.class.name
|
|
67
|
+
end
|
|
77
68
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
root_definition: @definition,
|
|
82
|
-
component: @component_instance
|
|
83
|
-
)
|
|
69
|
+
def state_payload
|
|
70
|
+
@state.to_h.to_json
|
|
71
|
+
end
|
|
84
72
|
|
|
85
|
-
|
|
73
|
+
def definition_payload
|
|
74
|
+
@definition.to_h.to_json
|
|
86
75
|
end
|
|
87
76
|
end
|
|
88
77
|
end
|
|
@@ -6,15 +6,18 @@ module Tailmix
|
|
|
6
6
|
def self.build(definition)
|
|
7
7
|
Class.new(Tailmix::Runtime::Context) do
|
|
8
8
|
definition.elements.each_key do |element_name|
|
|
9
|
-
define_method(element_name) do
|
|
10
|
-
attributes_for(element_name
|
|
9
|
+
define_method(element_name) do
|
|
10
|
+
attributes_for(element_name)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
alias_method :state, :state_proxy
|
|
15
|
+
alias_method :action, :action_proxy
|
|
16
|
+
|
|
14
17
|
def inspect
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"#<Tailmix::UI for #{component_name}
|
|
18
|
+
# elements_list = @definition.elements.keys.join(", ")
|
|
19
|
+
# "#<Tailmix::UI for #{component_name} elements=[#{elements_list}] dimensions=#{@dimensions.inspect}>"
|
|
20
|
+
"#<Tailmix::UI for #{component_name} state=#{get_state.inspect}>"
|
|
18
21
|
end
|
|
19
22
|
end
|
|
20
23
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Runtime
|
|
5
|
+
class State
|
|
6
|
+
def initialize(state_definition, initial_values, cache:)
|
|
7
|
+
@definition = state_definition
|
|
8
|
+
@cache = cache
|
|
9
|
+
@data = initialize_data(initial_values)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](key)
|
|
13
|
+
@data[key.to_sym]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def []=(key, value)
|
|
17
|
+
return if @data[key.to_sym] == value
|
|
18
|
+
|
|
19
|
+
@data[key.to_sym] = value
|
|
20
|
+
# Main reactive trigger: when the state changes – we clear the cache!
|
|
21
|
+
@cache.clear!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
@data
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def initialize_data(initial_values)
|
|
31
|
+
defaults = @definition.transform_values { |v| v[:default] }
|
|
32
|
+
defaults.merge(initial_values.transform_keys(&:to_sym))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Runtime
|
|
5
|
+
# Proxy for convenient access to the component state (ui.state).
|
|
6
|
+
class StateProxy
|
|
7
|
+
def initialize(context)
|
|
8
|
+
@context = context
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def method_missing(method_name, *args, &block)
|
|
12
|
+
state_key = method_name.to_s.chomp("=").to_sym
|
|
13
|
+
|
|
14
|
+
# We are checking if this state is defined in the DSL.
|
|
15
|
+
unless @context.definition.states.key?(state_key)
|
|
16
|
+
raise NoMethodError, "undefined state `#{state_key}` for #{@context.component_name}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if method_name.end_with?("=")
|
|
20
|
+
# This is a setter: ui.state.open = true
|
|
21
|
+
@context.set_state(state_key, args.first)
|
|
22
|
+
else
|
|
23
|
+
# This is a getter: ui.state.open
|
|
24
|
+
@context.get_state(state_key)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
29
|
+
state_key = method_name.to_s.chomp("=").to_sym
|
|
30
|
+
@context.definition.states.key?(state_key) || super
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/tailmix/runtime.rb
CHANGED
data/lib/tailmix/version.rb
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
##
|
|
5
|
+
# Provides helper methods for rendering Tailmix component definitions and managing
|
|
6
|
+
# Tailmix-related data attributes in view templates.
|
|
7
|
+
module ViewHelpers
|
|
8
|
+
# Renders a script tag containing the definitions for all unique
|
|
9
|
+
# Tailmix components used on the current page.
|
|
10
|
+
def tailmix_definitions_tag
|
|
11
|
+
definitions = Tailmix::Registry.instance.definitions
|
|
12
|
+
return if definitions.empty?
|
|
13
|
+
|
|
14
|
+
json_payload = definitions.to_json
|
|
15
|
+
|
|
16
|
+
tag.script(
|
|
17
|
+
type: "application/json",
|
|
18
|
+
"data-tailmix-definitions": "true"
|
|
19
|
+
) do
|
|
20
|
+
json_payload.html_safe
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Generates a hash of attributes for an external trigger that will control a named component instance.
|
|
25
|
+
#
|
|
26
|
+
# @param target_id [String, Symbol] Target component ID.
|
|
27
|
+
# @param action_name [String, Symbol] The name of the action to be called.
|
|
28
|
+
# @param options [Hash]
|
|
29
|
+
# @return [Hash]
|
|
30
|
+
def tailmix_trigger_for(target_id, action_name, options = {})
|
|
31
|
+
# target_id = options.fetch(:target_id)
|
|
32
|
+
# action_name = options.fetch(:action_name)
|
|
33
|
+
|
|
34
|
+
event_name = options.fetch(:event_name, :click)
|
|
35
|
+
with = options.fetch(:with, nil)
|
|
36
|
+
|
|
37
|
+
attributes = {
|
|
38
|
+
"data-tailmix-trigger-for" => target_id.to_s,
|
|
39
|
+
"data-tailmix-action" => "#{event_name}->#{action_name}"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if with
|
|
43
|
+
attributes["data-tailmix-action-payload"] = with.to_json
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
attributes
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/tailmix.rb
CHANGED
|
@@ -5,6 +5,8 @@ require_relative "tailmix/configuration"
|
|
|
5
5
|
require_relative "tailmix/dsl"
|
|
6
6
|
require_relative "tailmix/definition"
|
|
7
7
|
require_relative "tailmix/runtime"
|
|
8
|
+
require_relative "tailmix/middleware/registry_cleaner"
|
|
9
|
+
require_relative "tailmix/view_helpers"
|
|
8
10
|
|
|
9
11
|
module Tailmix
|
|
10
12
|
class Error < StandardError; end
|
|
@@ -25,8 +27,8 @@ module Tailmix
|
|
|
25
27
|
base.extend(DSL)
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
def tailmix(
|
|
29
|
-
self.class.tailmix_facade_class.new(self, self.class.tailmix_definition,
|
|
30
|
+
def tailmix(id: nil, **initial_state)
|
|
31
|
+
self.class.tailmix_facade_class.new(self, self.class.tailmix_definition, initial_state, id: id)
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|