tailmix 0.4.6 → 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 +76 -145
- 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 +39 -0
- data/examples/_modal_component.arb +17 -25
- data/examples/button.rb +81 -0
- data/examples/helpers.rb +25 -0
- data/examples/modal_component.rb +32 -164
- 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 +19 -13
- data/lib/tailmix/definition/merger.rb +2 -1
- data/lib/tailmix/definition/payload_proxy.rb +16 -0
- data/lib/tailmix/definition/result.rb +17 -18
- data/lib/tailmix/dev/docs.rb +32 -7
- 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 -47
- 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 +34 -21
- 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/lib/tailmix/definition/contexts/action_builder.rb +0 -31
- data/lib/tailmix/definition/contexts/element_builder.rb +0 -41
- 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
data/examples/modal_component.rb
CHANGED
|
@@ -1,201 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../lib/tailmix"
|
|
4
|
+
require_relative "helpers"
|
|
4
5
|
|
|
5
6
|
class ModalComponent
|
|
6
7
|
include Tailmix
|
|
7
8
|
attr_reader :ui
|
|
8
9
|
|
|
9
10
|
tailmix do
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
variant true, "visible opacity-100"
|
|
13
|
-
variant false, "invisible opacity-0"
|
|
14
|
-
end
|
|
15
|
-
stimulus.controller("modal").action_payload(:toggle, as: :toggle_data)
|
|
16
|
-
end
|
|
11
|
+
plugin :auto_focus, on: :open_button, delay: 100
|
|
12
|
+
state :open, default: false, toggle: true
|
|
17
13
|
|
|
18
|
-
element :
|
|
19
|
-
stimulus.context("modal").action(:click, :close)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
element :panel, "relative bg-white rounded-lg shadow-xl transition-transform transform" do
|
|
23
|
-
dimension :size, default: :md do
|
|
24
|
-
variant :sm, "w-full max-w-sm p-4" do
|
|
25
|
-
classes "dark:text-slate-400", group: :dark_mode
|
|
26
|
-
classes "one two"
|
|
27
|
-
end
|
|
28
|
-
variant :md, "w-full max-w-md p-6"
|
|
29
|
-
variant :lg, "w-full max-w-lg p-8"
|
|
30
|
-
end
|
|
31
|
-
stimulus.context("modal").target("panel")
|
|
14
|
+
element :container do
|
|
32
15
|
end
|
|
33
16
|
|
|
34
|
-
element :
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
stimulus.context("modal").action(:click, :close)
|
|
17
|
+
element :open_button do
|
|
18
|
+
# We attach the `click` event to our auto-generated action.
|
|
19
|
+
on :click, :toggle_open
|
|
38
20
|
end
|
|
39
21
|
|
|
40
|
-
element :
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
22
|
+
element :base do
|
|
23
|
+
dimension :open do
|
|
24
|
+
variant true, "fixed inset-0 z-50 flex items-center justify-center visible opacity-100 transition-opacity"
|
|
25
|
+
variant false, "invisible opacity-0"
|
|
26
|
+
end
|
|
45
27
|
end
|
|
46
28
|
|
|
47
|
-
element :
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
classes "hidden"
|
|
52
|
-
end
|
|
53
|
-
element :panel do
|
|
54
|
-
classes "hidden"
|
|
29
|
+
element :overlay do
|
|
30
|
+
dimension :open do
|
|
31
|
+
variant true, "fixed inset-0 bg-black/50"
|
|
32
|
+
variant false, "hidden"
|
|
55
33
|
end
|
|
34
|
+
on :click, :toggle_open
|
|
56
35
|
end
|
|
57
36
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
element :panel do
|
|
63
|
-
data locked: true, reason: "processing"
|
|
37
|
+
element :panel, "relative bg-white rounded-lg shadow-xl" do
|
|
38
|
+
dimension :open do
|
|
39
|
+
variant true, "block"
|
|
40
|
+
variant false, "hidden"
|
|
64
41
|
end
|
|
65
42
|
end
|
|
66
43
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
classes "opacity-75 cursor-not-allowed"
|
|
70
|
-
end
|
|
71
|
-
element :spinner do
|
|
72
|
-
classes "flex"
|
|
73
|
-
end
|
|
44
|
+
element :close_button, "absolute top-2 right-2 p-1 text-gray-500 rounded-full cursor-pointer" do
|
|
45
|
+
on :click, :toggle_open
|
|
74
46
|
end
|
|
75
|
-
end
|
|
76
47
|
|
|
77
|
-
|
|
78
|
-
|
|
48
|
+
element :title, "text-lg font-semibold text-gray-900 p-4 border-b"
|
|
49
|
+
element :body, "p-4 text-gray-900"
|
|
79
50
|
end
|
|
80
51
|
|
|
81
|
-
def
|
|
82
|
-
@ui
|
|
52
|
+
def initialize(open: false, id: nil)
|
|
53
|
+
@ui = tailmix(open: open, id: id)
|
|
83
54
|
end
|
|
84
55
|
end
|
|
85
56
|
|
|
86
|
-
puts "-" * 100
|
|
87
|
-
# puts ModalComponent.dev.docs
|
|
88
|
-
# puts ""
|
|
89
|
-
# puts "Scaffolds:"
|
|
90
|
-
# puts ""
|
|
91
|
-
# puts ModalComponent.dev.stimulus.scaffold
|
|
92
|
-
# puts ""
|
|
93
|
-
|
|
94
|
-
# >>>
|
|
95
|
-
#
|
|
96
|
-
# == Tailmix Docs for ModalComponent ==
|
|
97
|
-
# Signature: `initialize(open: true, size: :md)`
|
|
98
|
-
#
|
|
99
|
-
# Dimensions:
|
|
100
|
-
# - open (default: true)
|
|
101
|
-
# - true:
|
|
102
|
-
# - classes : "visible opacity-100"
|
|
103
|
-
# - false:
|
|
104
|
-
# - classes : "invisible opacity-0"
|
|
105
|
-
# - size (default: :md)
|
|
106
|
-
# - :sm:
|
|
107
|
-
# - classes : "w-full max-w-sm p-4"
|
|
108
|
-
# - classes (group: :dark_mode): "dark:text-slate-400"
|
|
109
|
-
# - classes : "one two"
|
|
110
|
-
# - :md:
|
|
111
|
-
# - classes : "w-full max-w-md p-6"
|
|
112
|
-
# - :lg:
|
|
113
|
-
# - classes : "w-full max-w-lg p-8"
|
|
114
|
-
#
|
|
115
|
-
# Actions:
|
|
116
|
-
# - :toggle
|
|
117
|
-
# - :lock
|
|
118
|
-
# - :enter_pending_state
|
|
119
|
-
#
|
|
120
|
-
# Stimulus:
|
|
121
|
-
# - on `modal` controller:
|
|
122
|
-
# - Targets: panel
|
|
123
|
-
# - Actions: close
|
|
124
|
-
#
|
|
125
|
-
# Stimulus:
|
|
126
|
-
# - on `form-submission` controller:
|
|
127
|
-
# - Actions: submit
|
|
128
|
-
#
|
|
129
|
-
# Scaffolds:
|
|
130
|
-
#
|
|
131
|
-
# // Generated by Tailmix for the "modal" controller
|
|
132
|
-
# // Path: app/javascript/controllers/modal_controller.js
|
|
133
|
-
# import { Controller } from "@hotwired/stimulus"
|
|
134
|
-
# import Tailmix from "tailmix"
|
|
135
|
-
#
|
|
136
|
-
# export default class extends Controller {
|
|
137
|
-
# static targets = ['panel']
|
|
138
|
-
# static values = { toggleData: Object }
|
|
139
|
-
#
|
|
140
|
-
# connect() {
|
|
141
|
-
# console.log("modal controller connected to", this.element);
|
|
142
|
-
# }
|
|
143
|
-
# toggle(event) {
|
|
144
|
-
# if (event) event.preventDefault();
|
|
145
|
-
# Tailmix.run({ config: this.toggleDataValue, controller: this });
|
|
146
|
-
# }
|
|
147
|
-
#
|
|
148
|
-
# close() {
|
|
149
|
-
# console.log('modal#close fired');
|
|
150
|
-
# }
|
|
151
|
-
# }
|
|
152
|
-
# ------------------------------------------------------------
|
|
153
|
-
#
|
|
154
|
-
# // Generated by Tailmix for the "form-submission" controller
|
|
155
|
-
# // Path: app/javascript/controllers/form-submission_controller.js
|
|
156
|
-
# import { Controller } from "@hotwired/stimulus"
|
|
157
|
-
# import Tailmix from "tailmix"
|
|
158
|
-
#
|
|
159
|
-
# export default class extends Controller {
|
|
160
|
-
# static targets = []
|
|
161
|
-
# static values = { pendingData: Object }
|
|
162
|
-
#
|
|
163
|
-
# connect() {
|
|
164
|
-
# console.log("form-submission controller connected to", this.element);
|
|
165
|
-
# }
|
|
166
|
-
# enterPendingState(event) {
|
|
167
|
-
# if (event) event.preventDefault();
|
|
168
|
-
# Tailmix.run({ config: this.pendingDataValue, controller: this });
|
|
169
|
-
# }
|
|
170
|
-
#
|
|
171
|
-
# submit() {
|
|
172
|
-
# console.log('form-submission#submit fired');
|
|
173
|
-
# }
|
|
174
|
-
# }
|
|
175
|
-
# ------------------------------------------------------------
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
modal = ModalComponent.new(size: :lg, open: true)
|
|
181
|
-
# modal.lock!
|
|
182
|
-
ui = modal.ui
|
|
183
57
|
|
|
58
|
+
modal = ModalComponent.new(open: false, id: :user_profile_modal)
|
|
59
|
+
ui = modal.ui
|
|
184
60
|
|
|
185
|
-
def stringify_keys(obj)
|
|
186
|
-
case obj
|
|
187
|
-
when Hash
|
|
188
|
-
obj.transform_keys(&:to_s).transform_values { |v| stringify_keys(v) }
|
|
189
|
-
when Array
|
|
190
|
-
obj.map { |v| stringify_keys(v) }
|
|
191
|
-
else
|
|
192
|
-
obj
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
61
|
|
|
196
62
|
# puts "Definition:"
|
|
197
63
|
# puts JSON.pretty_generate(stringify_keys(ModalComponent.tailmix_definition.to_h))
|
|
198
|
-
|
|
64
|
+
puts "-" * 100
|
|
65
|
+
puts ModalComponent.dev.docs
|
|
66
|
+
puts "-" * 100
|
|
199
67
|
|
|
200
68
|
ModalComponent.dev.elements.each do |element_name|
|
|
201
69
|
element = ui.send(element_name)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "../payload_proxy"
|
|
3
|
+
|
|
4
|
+
module Tailmix
|
|
5
|
+
module Definition
|
|
6
|
+
module Builders
|
|
7
|
+
class ActionBuilder
|
|
8
|
+
def initialize
|
|
9
|
+
@transitions = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def set(state_key, value)
|
|
13
|
+
processed_value = if value.is_a?(PayloadValue)
|
|
14
|
+
{ __type: 'payload_value', key: value.key }
|
|
15
|
+
else
|
|
16
|
+
value
|
|
17
|
+
end
|
|
18
|
+
@transitions << { type: :set, payload: { key: state_key.to_sym, value: processed_value } }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def toggle(state_key)
|
|
22
|
+
@transitions << { type: :toggle, payload: { key: state_key.to_sym } }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def refresh(state_key, params: {})
|
|
26
|
+
@transitions << { type: :refresh, payload: { key: state_key.to_sym, params: params } }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dispatch(event_name, detail: {})
|
|
30
|
+
@transitions << { type: :dispatch, payload: { name: event_name, detail: detail } }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def build_definition
|
|
34
|
+
Result::Action.new(transitions: @transitions.freeze)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "action_builder"
|
|
4
|
+
require_relative "element_builder"
|
|
5
|
+
require_relative "reactor_builder"
|
|
6
|
+
require_relative "../payload_proxy"
|
|
7
|
+
|
|
8
|
+
module Tailmix
|
|
9
|
+
module Definition
|
|
10
|
+
module Builders
|
|
11
|
+
class ComponentBuilder
|
|
12
|
+
attr_reader :component_name
|
|
13
|
+
|
|
14
|
+
def initialize(component_name:)
|
|
15
|
+
@states = {}
|
|
16
|
+
@actions = {}
|
|
17
|
+
@elements = {}
|
|
18
|
+
@component_name = component_name
|
|
19
|
+
@plugins = {}
|
|
20
|
+
@reactions = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def state(name, default: nil, endpoint: nil, toggle: false)
|
|
24
|
+
@states[name.to_sym] = { default: default, endpoint: endpoint }.compact
|
|
25
|
+
if toggle
|
|
26
|
+
action_name = :"toggle_#{name}"
|
|
27
|
+
action(action_name) { toggle name }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def action(name, &block)
|
|
32
|
+
builder = ActionBuilder.new
|
|
33
|
+
builder.instance_exec(PayloadProxy.new, &block)
|
|
34
|
+
@actions[name.to_sym] = builder
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def element(name, classes = "", &block)
|
|
38
|
+
builder = ElementBuilder.new(name)
|
|
39
|
+
builder.attributes.classes(classes.split)
|
|
40
|
+
builder.instance_eval(&block) if block
|
|
41
|
+
@elements[name.to_sym] = builder
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def plugin(name, options = {})
|
|
45
|
+
plugin_name = name.to_s.gsub(/_([a-z])/) { $1.upcase }
|
|
46
|
+
@plugins[plugin_name] = options
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def react(on:, run: nil, **options, &block)
|
|
50
|
+
watched_states = Array(on)
|
|
51
|
+
|
|
52
|
+
# Processing the short form: `react on: :query, run: :search`
|
|
53
|
+
if run
|
|
54
|
+
builder = ReactorBuilder.new(watched_states.first)
|
|
55
|
+
builder.run(run, **options)
|
|
56
|
+
watched_states.each { |state| (@reactions[state] ||= []).concat(builder.build_rules) }
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Processing the full form with the block.
|
|
61
|
+
if block
|
|
62
|
+
builder = ReactorBuilder.new(watched_states.first)
|
|
63
|
+
builder.instance_eval(&block) # `instance_eval` чтобы не писать `r.`
|
|
64
|
+
watched_states.each { |state| (@reactions[state] ||= []).concat(builder.build_rules) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_definition
|
|
69
|
+
Result::Context.new(
|
|
70
|
+
name: component_name,
|
|
71
|
+
states: @states.freeze,
|
|
72
|
+
actions: @actions.transform_values(&:build_definition).freeze,
|
|
73
|
+
elements: @elements.transform_values(&:build_definition).freeze,
|
|
74
|
+
reactions: @reactions.freeze,
|
|
75
|
+
plugins: @plugins.freeze
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -4,7 +4,7 @@ require_relative "variant_builder"
|
|
|
4
4
|
|
|
5
5
|
module Tailmix
|
|
6
6
|
module Definition
|
|
7
|
-
module
|
|
7
|
+
module Builders
|
|
8
8
|
class DimensionBuilder
|
|
9
9
|
def initialize(default: nil)
|
|
10
10
|
@variants = {}
|
|
@@ -13,12 +13,12 @@ module Tailmix
|
|
|
13
13
|
|
|
14
14
|
def variant(name, classes = "", data: {}, aria: {}, &block)
|
|
15
15
|
builder = VariantBuilder.new
|
|
16
|
+
|
|
16
17
|
builder.classes(classes) if classes && !classes.empty?
|
|
17
18
|
builder.data(data)
|
|
18
19
|
builder.aria(aria)
|
|
19
20
|
|
|
20
21
|
builder.instance_eval(&block) if block
|
|
21
|
-
|
|
22
22
|
@variants[name] = builder.build_variant
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "attribute_builder"
|
|
4
|
+
require_relative "dimension_builder"
|
|
5
|
+
require_relative "variant_builder"
|
|
6
|
+
|
|
7
|
+
module Tailmix
|
|
8
|
+
module Definition
|
|
9
|
+
module Builders
|
|
10
|
+
class ElementBuilder
|
|
11
|
+
def initialize(name)
|
|
12
|
+
@name = name
|
|
13
|
+
@default_attributes = {}
|
|
14
|
+
@dimensions = {}
|
|
15
|
+
@compound_variants = []
|
|
16
|
+
@event_bindings = []
|
|
17
|
+
@attribute_bindings = {}
|
|
18
|
+
@model_bindings = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def attributes
|
|
22
|
+
@attributes_builder ||= AttributeBuilder.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def method_missing(name, *args, &block)
|
|
26
|
+
attribute_name = name.to_s.chomp("=").to_sym
|
|
27
|
+
@default_attributes[attribute_name] = args.first
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def respond_to_missing?(*_args)
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on(event_name, action_name, with: nil, **options)
|
|
35
|
+
# `with` mapping: `{ payload_key => state_key }`
|
|
36
|
+
@event_bindings << { event: event_name, action: action_name, with: with, options: options }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def bind(attribute_name, to:)
|
|
40
|
+
@attribute_bindings[attribute_name.to_sym] = to.to_sym
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def model(attribute_name, to:, on: :input, action: nil, debounce: nil)
|
|
44
|
+
@model_bindings[attribute_name.to_sym] = {
|
|
45
|
+
state: to.to_sym,
|
|
46
|
+
event: on,
|
|
47
|
+
action: action,
|
|
48
|
+
debounce: debounce
|
|
49
|
+
}.compact
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def dimension(name, &block)
|
|
53
|
+
builder = DimensionBuilder.new
|
|
54
|
+
builder.instance_eval(&block)
|
|
55
|
+
@dimensions[name.to_sym] = builder.build_dimension
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def compound_variant(on:, &block)
|
|
59
|
+
builder = VariantBuilder.new
|
|
60
|
+
builder.instance_eval(&block)
|
|
61
|
+
|
|
62
|
+
@compound_variants << {
|
|
63
|
+
on: on,
|
|
64
|
+
modifications: builder.build_variant
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_definition
|
|
69
|
+
Result::Element.new(
|
|
70
|
+
name: @name,
|
|
71
|
+
attributes: attributes.build_definition,
|
|
72
|
+
default_attributes: @default_attributes.freeze,
|
|
73
|
+
dimensions: @dimensions.freeze,
|
|
74
|
+
compound_variants: @compound_variants.freeze,
|
|
75
|
+
event_bindings: @event_bindings.freeze,
|
|
76
|
+
attribute_bindings: @attribute_bindings.freeze,
|
|
77
|
+
model_bindings: @model_bindings.freeze
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "rule_builder"
|
|
3
|
+
|
|
4
|
+
module Tailmix
|
|
5
|
+
module Definition
|
|
6
|
+
module Builders
|
|
7
|
+
class ReactorBuilder
|
|
8
|
+
def initialize(watched_state)
|
|
9
|
+
@watched_state = watched_state
|
|
10
|
+
@rules = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Start a method for the rule chain: r.value("commercial")
|
|
14
|
+
def value(expected_value)
|
|
15
|
+
rule_builder = RuleBuilder.new(@watched_state)
|
|
16
|
+
rule_builder.value(expected_value)
|
|
17
|
+
@rules << rule_builder
|
|
18
|
+
rule_builder
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Alternative startup method: r.state(:zip_code)
|
|
22
|
+
def state(state_key)
|
|
23
|
+
rule_builder = RuleBuilder.new(state_key)
|
|
24
|
+
@rules << rule_builder
|
|
25
|
+
rule_builder
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Unconditional effect (always triggers on change)
|
|
29
|
+
def run(action_name, with: nil)
|
|
30
|
+
# We create an "empty" rule with a condition that is always true.
|
|
31
|
+
rule_builder = RuleBuilder.new(nil)
|
|
32
|
+
rule_builder.instance_variable_set(:@rule, { condition: { type: :always_true } })
|
|
33
|
+
rule_builder.run(action_name, with: with)
|
|
34
|
+
@rules << rule_builder
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_rules
|
|
38
|
+
@rules.map(&:build_rule)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Definition
|
|
5
|
+
module Builders
|
|
6
|
+
class RuleBuilder
|
|
7
|
+
def initialize(source_state_key)
|
|
8
|
+
@rule = { condition: { type: :eql, source: { type: :state, key: source_state_key } } }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def value(expected_value)
|
|
12
|
+
@rule[:condition][:value] = expected_value
|
|
13
|
+
self
|
|
14
|
+
end
|
|
15
|
+
alias_method :is, :value
|
|
16
|
+
|
|
17
|
+
def is_not(expected_value)
|
|
18
|
+
@rule[:condition][:type] = :not_eql
|
|
19
|
+
@rule[:condition][:value] = expected_value
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def is_truthy
|
|
24
|
+
@rule[:condition][:type] = :truthy
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def set_state(payload)
|
|
29
|
+
add_effect(:set_state, payload: payload)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run(action_name, with: nil)
|
|
33
|
+
add_effect(:run_action, name: action_name, with: with)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def dispatch(event_name, detail: {})
|
|
37
|
+
add_effect(:dispatch_event, name: event_name, detail: detail)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call(element_name, method_name, *args)
|
|
41
|
+
add_effect(:call_method, element: element_name, method: method_name, args: args)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_rule
|
|
45
|
+
@rule
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def add_effect(type, **payload)
|
|
51
|
+
@rule[:effects] ||= []
|
|
52
|
+
@rule[:effects] << { type: type, payload: payload }
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tailmix
|
|
4
|
+
module Definition
|
|
5
|
+
module Builders
|
|
6
|
+
class StateBuilder
|
|
7
|
+
def initialize
|
|
8
|
+
@data_source = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def endpoint(method, url:)
|
|
12
|
+
@data_source = { method: method, url: url }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_data_source
|
|
16
|
+
@data_source.empty? ? nil : @data_source.freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Tailmix
|
|
4
4
|
module Definition
|
|
5
|
-
module
|
|
5
|
+
module Builders
|
|
6
6
|
class VariantBuilder
|
|
7
7
|
def initialize
|
|
8
8
|
@class_groups = []
|
|
9
9
|
@data = {}
|
|
10
10
|
@aria = {}
|
|
11
|
+
@attributes = {}
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def classes(class_string, options = {})
|
|
@@ -22,11 +23,25 @@ module Tailmix
|
|
|
22
23
|
@aria.merge!(hash)
|
|
23
24
|
end
|
|
24
25
|
|
|
26
|
+
def method_missing(name, *args, &block)
|
|
27
|
+
# `disabled true` -> { disabled: true }
|
|
28
|
+
# `placeholder "text"` -> { placeholder: "text" }
|
|
29
|
+
# `type "password"` -> { type: "password" }
|
|
30
|
+
attribute_name = name.to_s.chomp("=").to_sym
|
|
31
|
+
value = args.first
|
|
32
|
+
@attributes[attribute_name] = value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def respond_to_missing?(*_args)
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
25
39
|
def build_variant
|
|
26
40
|
Definition::Result::Variant.new(
|
|
27
41
|
class_groups: @class_groups.freeze,
|
|
28
42
|
data: @data.freeze,
|
|
29
|
-
aria: @aria.freeze
|
|
43
|
+
aria: @aria.freeze,
|
|
44
|
+
attributes: @attributes.freeze
|
|
30
45
|
)
|
|
31
46
|
end
|
|
32
47
|
end
|