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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +46 -205
  4. data/app/javascript/tailmix/runtime/action_dispatcher.js +132 -0
  5. data/app/javascript/tailmix/runtime/component.js +130 -0
  6. data/app/javascript/tailmix/runtime/index.js +130 -0
  7. data/app/javascript/tailmix/runtime/plugins.js +35 -0
  8. data/app/javascript/tailmix/runtime/updater.js +140 -0
  9. data/docs/01_getting_started.md +0 -81
  10. data/examples/_modal_component.arb +17 -25
  11. data/examples/modal_component.rb +31 -155
  12. data/lib/tailmix/definition/builders/action_builder.rb +39 -0
  13. data/lib/tailmix/definition/{contexts → builders}/actions/element_builder.rb +1 -1
  14. data/lib/tailmix/definition/{contexts → builders}/attribute_builder.rb +1 -1
  15. data/lib/tailmix/definition/builders/component_builder.rb +81 -0
  16. data/lib/tailmix/definition/{contexts → builders}/dimension_builder.rb +2 -2
  17. data/lib/tailmix/definition/builders/element_builder.rb +83 -0
  18. data/lib/tailmix/definition/builders/reactor_builder.rb +43 -0
  19. data/lib/tailmix/definition/builders/rule_builder.rb +58 -0
  20. data/lib/tailmix/definition/builders/state_builder.rb +21 -0
  21. data/lib/tailmix/definition/{contexts → builders}/variant_builder.rb +17 -2
  22. data/lib/tailmix/definition/context_builder.rb +17 -12
  23. data/lib/tailmix/definition/payload_proxy.rb +16 -0
  24. data/lib/tailmix/definition/result.rb +17 -19
  25. data/lib/tailmix/dev/docs.rb +3 -9
  26. data/lib/tailmix/dev/tools.rb +0 -5
  27. data/lib/tailmix/dsl.rb +3 -5
  28. data/lib/tailmix/engine.rb +11 -1
  29. data/lib/tailmix/html/attributes.rb +22 -12
  30. data/lib/tailmix/middleware/registry_cleaner.rb +17 -0
  31. data/lib/tailmix/registry.rb +37 -0
  32. data/lib/tailmix/runtime/action_proxy.rb +29 -0
  33. data/lib/tailmix/runtime/attribute_builder.rb +102 -0
  34. data/lib/tailmix/runtime/attribute_cache.rb +23 -0
  35. data/lib/tailmix/runtime/context.rb +51 -62
  36. data/lib/tailmix/runtime/facade_builder.rb +8 -5
  37. data/lib/tailmix/runtime/state.rb +36 -0
  38. data/lib/tailmix/runtime/state_proxy.rb +34 -0
  39. data/lib/tailmix/runtime.rb +0 -1
  40. data/lib/tailmix/version.rb +1 -1
  41. data/lib/tailmix/view_helpers.rb +49 -0
  42. data/lib/tailmix.rb +4 -2
  43. metadata +26 -20
  44. data/app/javascript/tailmix/finder.js +0 -15
  45. data/app/javascript/tailmix/index.js +0 -7
  46. data/app/javascript/tailmix/mutator.js +0 -28
  47. data/app/javascript/tailmix/runner.js +0 -7
  48. data/app/javascript/tailmix/stimulus_adapter.js +0 -37
  49. data/docs/02_dsl_reference.md +0 -266
  50. data/docs/03_advanced_usage.md +0 -88
  51. data/docs/04_client_side_bridge.md +0 -119
  52. data/docs/05_cookbook.md +0 -249
  53. data/lib/tailmix/definition/contexts/action_builder.rb +0 -31
  54. data/lib/tailmix/definition/contexts/element_builder.rb +0 -55
  55. data/lib/tailmix/definition/contexts/stimulus_builder.rb +0 -101
  56. data/lib/tailmix/dev/stimulus_generator.rb +0 -124
  57. data/lib/tailmix/runtime/stimulus/compiler.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 972f0dcd1667c6db95fbce07209fdf7ab3568b335fd0dab933977a641114dc6a
4
- data.tar.gz: 3063e23f5422ec1fe5f20683263a6b7cfce4f9c054d393e520f962a0727d4992
3
+ metadata.gz: 25625758329e8524ce4fa37154be93d929b43ce85dfc5bb5669e8646b82ebd88
4
+ data.tar.gz: 1a98f9b38e7799819579309691e6692ec94ac5cf1358159c6c6431cc1d16c920
5
5
  SHA512:
6
- metadata.gz: fb3cfcfef0f342f12dc680a18e7a7d20b278b08699f28b09b473f8843747d147cd9a1ae56db112a86ce4c52881b609ecd6a9fe863fcdd43c9b63ff0ccc3f50cc
7
- data.tar.gz: 75366e305721e6091324578ff4138f10b1d0ed70135d0094f901bb52f58580465f528731b498e23f2f61e26369dd4ad5d34425d28da81e26996c138c1d2872b9
6
+ metadata.gz: 3761c68a9b82be223d3c78f5e201347c9554165eceb42e3c00b2191c5cd7f6a128f5fd251ff5bd26328118d962f136d10ca552acfa5ee49a5819035910c27c9b
7
+ data.tar.gz: 21fc2e32708b3bb8c8ed8d14b83535d5f51d17d78abe6eec14138283e23b17059b3b2c76799cebd019e9f6578f0057cb8dfa4cd49aaa8aabddbdc859ea7af852
data/CHANGELOG.md CHANGED
@@ -3,3 +3,11 @@
3
3
  ## [0.1.0] - 2025-08-12
4
4
 
5
5
  - Initial release
6
+
7
+ ## [0.4.8] - 2025-09-08
8
+
9
+ - New DSL
10
+ - State management
11
+ - Reactions
12
+ - Actions
13
+ - Plugin system
data/README.md CHANGED
@@ -11,22 +11,9 @@ Inspired by modern frontend tools like CVA (Class Variance Authority), Tailmix b
11
11
 
12
12
  * **Declarative DSL:** Describe your component's styles with an elegant and intuitive API.
13
13
  * **Variants & Compound Variants:** Easily manage different component states (`size`, `color`, etc.) and their combinations.
14
- * **Stimulus Bridge:** Seamlessly integrate with StimulusJS to create interactive components.
15
- * **"Hot" UI Updates:** Enable optimistic UI updates with `action` and `action_payload` in tandem with Hotwire/Turbo.
16
14
  * **Component Inheritance:** Create base components and extend them to avoid code duplication.
17
- * **Developer Tools:** Get built-in documentation and code generators right in your console.
18
15
  * **Zero Dependencies:** Pure Ruby, ready to work in any project.
19
16
 
20
- -----
21
-
22
- ## Quick Links
23
-
24
- * **[Full Documentation →](/docs)**
25
- * **[DSL Reference](/docs/02_dsl_reference.md)**
26
- * **[Cookbook (Recipes)](/docs/05_cookbook.md)**
27
-
28
-
29
- -----
30
17
 
31
18
  ## Installation
32
19
 
@@ -48,120 +35,10 @@ bin/rails g tailmix:install
48
35
 
49
36
  The core idea of Tailmix is to describe all variants of your component within a Ruby class.
50
37
 
51
- ### 1. Basic Example: The `Badge` Component
52
-
53
- Let's create a simple, flexible `Badge` that can have different colors.
54
-
55
- **Component Definition:**
56
-
57
- ```ruby
58
- # app/components/badge_component.rb
59
- class BadgeComponent
60
- include Tailmix
61
- attr_reader :ui, :text
62
-
63
- def initialize(text, color: :gray)
64
- @ui = tailmix(color: color)
65
- @text = text
66
- end
67
-
68
- tailmix do
69
- element :badge, "inline-flex items-center px-2.5 py-0.5 text-xs font-medium rounded-full" do
70
- dimension :color, default: :gray do
71
- variant :gray, "bg-gray-100 text-gray-800"
72
- variant :success, "bg-green-100 text-green-800"
73
- variant :danger, "bg-red-100 text-red-800"
74
- end
75
- end
76
- end
77
- end
78
- ```
79
-
80
- **Usage in ERB:**
81
- In ERB, use the `**` operator to pass the attributes.
82
-
83
- ```erb
84
- <% badge = BadgeComponent.new("Active", color: :success) %>
85
-
86
- <span <%= tag.attributes **badge.ui.badge %>>
87
- <%= badge.text %>
88
- </span>
89
- ```
90
-
91
- **Usage in Arbre:**
92
- Arbre was the primary inspiration for Tailmix. Integration is seamless and does not require the `**` operator.
93
-
94
- ```ruby
95
- # my_view.arb
96
- badge = BadgeComponent.new("Active", color: :success)
97
- ui = badge.ui
98
-
99
- span ui.badge do
100
- text_node badge.text
101
- end
102
- ```
103
-
104
- ### 2\. Adding Interactivity with Stimulus
105
-
106
- Tailmix allows you to declaratively define Stimulus controllers. Let's build a component to copy text to the clipboard.
38
+ ### 1. Basic Example: The `Modal` Component
107
39
 
108
40
  **Component Definition:**
109
41
 
110
- ```ruby
111
- # app/components/clipboard_component.rb
112
- class ClipboardComponent
113
- include Tailmix
114
- attr_reader :ui, :text_to_copy
115
-
116
- def initialize(text_to_copy)
117
- @ui = tailmix
118
- @text_to_copy = text_to_copy
119
- end
120
-
121
- tailmix do
122
- element :wrapper, "flex items-center space-x-2" do
123
- stimulus.controller("clipboard") # data-controller="clipboard"
124
- end
125
-
126
- element :source, "p-2 border rounded-md bg-gray-50" do
127
- stimulus.context("clipboard").target("source") # data-clipboard-target="source"
128
- end
129
-
130
- element :button, "px-3 py-2 text-sm font-medium text-white bg-blue-600 rounded-md" do
131
- stimulus.context("clipboard").action(:click, :copy) # data-action="click->clipboard#copy"
132
- end
133
- end
134
- end
135
- ```
136
-
137
- **Stimulus Controller:**
138
- Generate a template with `puts ClipboardComponent.dev.stimulus.scaffold` and fill in the logic.
139
-
140
- ```javascript
141
- // app/javascript/controllers/clipboard_controller.js
142
- import { Controller } from "@hotwired/stimulus"
143
-
144
- export default class extends Controller {
145
- static targets = ["source"]
146
-
147
- copy() {
148
- navigator.clipboard.writeText(this.sourceTarget.textContent)
149
- // Optionally: show a success notification
150
- }
151
- }
152
- ```
153
-
154
- Your component is now fully interactive, with its entire structure defined in a single Ruby file.
155
-
156
- -----
157
-
158
- ## 3. Advanced Example: An Interactive Modal
159
-
160
- Now let's see the full power of Tailmix by building a common UI pattern: a fully interactive modal component. This example combines multiple elements, shared variants, Stimulus integration, and a client-side action for optimistic updates.
161
-
162
- Component Definition:
163
- The `ModalComponent` definition is self-contained. It describes the structure, all possible states, and the interactive behavior of the modal.
164
-
165
42
  ```ruby
166
43
  # app/components/modal_component.rb
167
44
  class ModalComponent
@@ -169,132 +46,96 @@ class ModalComponent
169
46
  attr_reader :ui
170
47
 
171
48
  tailmix do
172
- # A container to hold the controller and its data
49
+ plugin :auto_focus, on: :open_button, delay: 100
50
+ state :open, default: false, toggle: true
51
+
173
52
  element :container do
174
- stimulus.controller("modal")
175
- .action_payload(:toggle, as: :toggle_data)
176
- # dynamic values:
177
- .value(:user_id, method: :get_current_user_id)
178
- .value(:generated_at, call: -> { Time.now.iso8601 })
179
53
  end
180
54
 
181
- # The button that will trigger the modal
182
- element :open_button, "inline-flex text-white bg-blue-600 rounded-lg px-5 py-2.5 cursor-pointer" do
183
- stimulus.context("modal").action(:click, :toggle)
55
+ element :open_button do
56
+ # We attach the `click` event to our auto-generated action.
57
+ on :click, :toggle_open
184
58
  end
185
59
 
186
- # The modal's backdrop and wrapper, controlled by the :open dimension
187
- element :base, "flex items-center justify-center" do
188
- dimension :open, default: false do
189
- variant true, "fixed inset-0 z-50 visible opacity-100 transition-opacity"
60
+ element :base do
61
+ dimension :open do
62
+ variant true, "fixed inset-0 z-50 flex items-center justify-center visible opacity-100 transition-opacity"
190
63
  variant false, "invisible opacity-0"
191
64
  end
192
65
  end
193
66
 
194
- element :overlay, "fixed inset-0 bg-black/50" do
195
- stimulus.context("modal").action(:click, :toggle)
67
+ element :overlay do
68
+ dimension :open do
69
+ variant true, "fixed inset-0 bg-black/50"
70
+ variant false, "hidden"
71
+ end
72
+ on :click, :toggle_open
196
73
  end
197
74
 
198
- # The main modal panel, with variants for both :open and :size
199
- element :panel, "w-full relative bg-white rounded-lg shadow-xl" do
200
- dimension :open, default: false do
75
+ element :panel, "relative bg-white rounded-lg shadow-xl" do
76
+ dimension :open do
201
77
  variant true, "block"
202
78
  variant false, "hidden"
203
79
  end
204
- dimension :size, default: :md do
205
- variant :sm, "max-w-sm p-4"
206
- variant :md, "max-w-md p-6"
207
- end
208
- stimulus.context("modal").target("panel")
209
80
  end
210
81
 
211
82
  element :close_button, "absolute top-2 right-2 p-1 text-gray-500 rounded-full cursor-pointer" do
212
- stimulus.context("modal").action(:click, :toggle)
83
+ on :click, :toggle_open
213
84
  end
214
85
 
215
- # The action that will be executed on the client-side
216
- action :toggle, method: :toggle do
217
- element :base do
218
- classes "visible opacity-100"
219
- classes "invisible opacity-0"
220
- end
221
- element :overlay do
222
- classes "fixed inset-0 bg-black/50"
223
- end
224
- element :panel do
225
- classes "block"
226
- classes "hidden"
227
- end
228
- end
86
+ element :title, "text-lg font-semibold text-gray-900 p-4 border-b"
87
+ element :body, "p-4 text-gray-900"
229
88
  end
230
89
 
231
- def initialize(size: :md, open: false)
232
- @ui = tailmix(size: size, open: open)
233
- end
234
-
235
- def get_current_user_id
236
- 123
90
+ def initialize(open: false, id: nil)
91
+ @ui = tailmix(open: open, id: id)
237
92
  end
238
93
  end
239
94
  ```
240
95
 
241
- **Stimulus Controller:**
242
- This controller will handle the modal's open/close logic and use the `action_payload` to trigger the closing animation defined in Ruby.
243
-
244
- ```javascript
245
- // app/javascript/controllers/modal_controller.js
246
- import { Controller } from "@hotwired/stimulus"
247
- import Tailmix from "tailmix"
248
-
249
- export default class extends Controller {
250
- static values = { toggleData: Object }
96
+ **Usage in ERB:**
97
+ In ERB, use the `**` operator to pass the attributes.
251
98
 
252
- toggle(event) {
253
- // Prevent default browser behavior, like form submissions
254
- if (event) event.preventDefault();
99
+ ```erb
100
+ <% ui = ModalComponent.new(open: false, id: :user_profile_modal).ui %>
255
101
 
256
- // Run the UI mutations defined in our Ruby :toggle action
257
- Tailmix.run({
258
- config: this.toggleDataValue,
259
- controller: this
260
- });
261
- }
262
- }
102
+ <div <%= tag.attributes **ui.container.component %>>
103
+ ...
104
+ </div>
263
105
  ```
264
106
 
265
- **View Usage (Arbre):**
266
- The view code is clean and readable. We instantiate the component and render its elements. The action_payload is serialized to the container element, making it available to the Stimulus controller.
107
+ **Usage in Arbre:**
108
+ Arbre was the primary inspiration for Tailmix. Integration is seamless and does not require the `**` operator.
267
109
 
268
110
  ```ruby
269
- # my_view.arb
270
- modal = ModalComponent.new(size: :sm)
271
- ui = modal.ui
111
+ # _example_modal_component.arb
112
+ modal_component = ModalComponent.new(open: false, id: :user_profile_modal)
113
+ ui = modal_component.ui
114
+
115
+ button "Open Modal Outer", tailmix_trigger_for(:user_profile_modal, :toggle_open)
272
116
 
273
- div ui.container do
117
+ div ui.container.component do
274
118
  button "Open Modal", ui.open_button
275
119
 
276
120
  div ui.base do
277
121
  div ui.overlay
122
+
278
123
  div ui.panel do
279
- button "Close", ui.close_button
280
- h3 "Payment Successful", ui.title
124
+ div ui.title do
125
+ h3 "Modal Title"
126
+ button ui.close_button do
127
+ text_node "✖"
128
+ end
129
+ end
281
130
 
282
131
  div ui.body do
283
- "Your payment has been successfully submitted..."
132
+ para "This is the main content of the modal. It's powered by the new Tailmix Runtime!"
284
133
  end
285
134
  end
286
135
  end
287
136
  end
288
137
  ```
289
138
 
290
- -----
291
-
292
- ## Developer Tools
293
-
294
- Use the `.dev` namespace to introspect your components right from the console.
295
-
296
- * **`YourComponent.dev.docs`**: Displays full documentation for all variants and actions.
297
- * **`YourComponent.dev.stimulus.scaffold`**: Generates a Stimulus controller template.
298
139
 
299
140
  ## Contributing
300
141
 
@@ -0,0 +1,132 @@
1
+ export class ActionDispatcher {
2
+
3
+ /**
4
+ * Creates an instance of ActionDispatcher.
5
+ * @param {Component} component - The component instance to which the dispatcher is attached.
6
+ */
7
+ constructor(component) {
8
+ this.component = component;
9
+ const internalActionElements = this.component.element.querySelectorAll('[data-tailmix-action]');
10
+ internalActionElements.forEach(element => this.bindAction(element));
11
+ }
12
+
13
+ /**
14
+ * Binds an action to a specific element based on its data-tailmix-action attribute.
15
+ * The attribute value should be in the format "eventName->actionName".
16
+ * @param {HTMLElement} element - The element to which the action is bound.
17
+ * @return {void}
18
+ */
19
+ bindAction(element) {
20
+ const actionString = element.dataset.tailmixAction;
21
+ const [eventName, actionName] = actionString.split('->');
22
+
23
+ if (!eventName || !actionName) {
24
+ console.warn(`Tailmix: Invalid action string "${actionString}"`);
25
+ return;
26
+ }
27
+
28
+ const actionDefinition = this.component.definition.actions[actionName];
29
+ if (!actionDefinition) {
30
+ console.warn(`Tailmix: Action "${actionName}" not found in definition.`);
31
+ return;
32
+ }
33
+
34
+ element.addEventListener(eventName, (event) => {
35
+ // Let's make sure the trigger is not part of another component.
36
+ if (element.dataset.tailmixTriggerFor && element.closest('[data-tailmix-component]') !== this.component.element) {
37
+ // Logic to prevent double triggering, if needed
38
+ }
39
+ this.dispatch(actionDefinition, event, element);
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Dispatches an action based on the provided action definition and event input.
45
+ * Processes each transition within the action definition and performs state updates
46
+ * or other specified operations on the component.
47
+ * @param {Object} actionDefinition - The definition of the action containing transitions to be processed.
48
+ * @param {Object} event - The event triggering the dispatch, providing context for the action.
49
+ * @param {HTMLElement} triggerElement - The element that triggered the action.
50
+ * @return {void}
51
+ */
52
+ dispatch(actionDefinition, event, triggerElement) {
53
+ let runtimePayload = {};
54
+ const withMapAttr = triggerElement.dataset.tailmixActionWith;
55
+ if (withMapAttr) {
56
+ const withMap = JSON.parse(withMapAttr);
57
+ for (const key in withMap) {
58
+ runtimePayload[key] = this.component.state[withMap[key]];
59
+ }
60
+ }
61
+
62
+ actionDefinition.transitions.forEach(transition => {
63
+ this.executeTransition(transition, runtimePayload, event);
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Executes a transition within an action definition.
69
+ * This method handles different transition types and applies updates to the component's state.'
70
+ * @param transition
71
+ * @param runtimePayload
72
+ * @param event
73
+ */
74
+ executeTransition(transition, runtimePayload, event) {
75
+ const {type, payload} = transition;
76
+ switch (type) {
77
+ case 'set':
78
+ const resolvedValue = this.resolveValue(payload.value, runtimePayload, event);
79
+ this.component.update({[payload.key]: resolvedValue});
80
+ break;
81
+ case 'toggle':
82
+ this.component.update({[payload.key]: !this.component.state[payload.key]});
83
+ break;
84
+ case 'refresh':
85
+ this.handleRefresh(payload, runtimePayload);
86
+ break;
87
+ case 'dispatch':
88
+ const detail = this.resolveValue(payload.detail, runtimePayload, event);
89
+ this.component.dispatch(payload.name, detail);
90
+ break;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Handles the refresh transition by fetching data from an endpoint based on the provided payload.
96
+ * @param {Object} payload - The payload containing the refresh configuration.
97
+ * @param {Object} runtimePayload - The runtime payload containing values to be used in the endpoint URL.
98
+ * @return {void}
99
+ */
100
+ handleRefresh(payload, runtimePayload) {
101
+ const stateDef = this.component.definition.states[payload.key];
102
+ if (!stateDef?.endpoint) {
103
+ console.warn(`Tailmix: No endpoint defined for state "${payload.key}"`);
104
+ return;
105
+ }
106
+
107
+ const params = new URLSearchParams();
108
+ if (payload.params) {
109
+ for (const key in payload.params) {
110
+ const stateKey = payload.params[key];
111
+ params.append(key, this.component.state[stateKey]);
112
+ }
113
+ }
114
+
115
+ const url = `${stateDef.endpoint.url}?${params.toString()}`;
116
+
117
+ // fetch(url, { headers: { 'Accept': 'text/vnd.turbo-stream.html' } })
118
+ // .then(r => r.text())
119
+ // .then(html => Turbo.renderStreamMessage(html));
120
+ console.log(`TODO: Fetch data for "${payload.key}" from ${url}`);
121
+ }
122
+
123
+ resolveValue(valueTemplate, runtimePayload, event) {
124
+ if (valueTemplate && valueTemplate.__type === 'payload_value') {
125
+ const keyPath = valueTemplate.key.split('.'); // e.g., "event.target.value"
126
+ let result = {event, ...runtimePayload};
127
+ keyPath.forEach(key => result = result?.[key]);
128
+ return result;
129
+ }
130
+ return valueTemplate;
131
+ }
132
+ }
@@ -0,0 +1,130 @@
1
+ import {ActionDispatcher} from './action_dispatcher';
2
+ import {Updater} from './updater';
3
+
4
+ /**
5
+ * Represents a component instance that manages the state and behavior of a specific UI element.
6
+ * The Component class is responsible for loading and initializing the component's state,
7
+ * finding and managing its elements, and updating the UI based on the component's state.
8
+ */
9
+ export class Component {
10
+ constructor(element, definition, pluginManager) {
11
+ this.element = element;
12
+ this.definition = definition;
13
+ this._state = this.loadInitialState();
14
+ this.elements = this.findElements();
15
+ this.updater = new Updater(this);
16
+ this.dispatcher = new ActionDispatcher(this);
17
+
18
+ // --- API ---
19
+ this.api = {
20
+ get state() {
21
+ return {...this._state};
22
+ },
23
+ setState: (newState) => this.update(newState),
24
+ element: (name) => this.elements[name],
25
+ runAction: (name, payload) => this.dispatcher.runActionByName(name, payload),
26
+ dispatch: (name, detail) => this.dispatch(name, detail),
27
+ on: (name, callback) => this.element.addEventListener(`tailmix:${name}`, callback),
28
+ };
29
+
30
+ this.initializeModels();
31
+ this.updater.run(this._state, {});
32
+ pluginManager.connect(this);
33
+
34
+ console.log(`Tailmix component "${this.definition.name || 'Unnamed'}" initialized.`, this);
35
+ }
36
+
37
+ /**
38
+ * Gets the current state of the component.
39
+ * @return {Object} The current state of the component.
40
+ */
41
+ get state() {
42
+ return this._state;
43
+ }
44
+
45
+ /**
46
+ * Updates the component's state with the provided new state and triggers the update mechanism.
47
+ * The previous state is logged for debugging purposes.
48
+ '
49
+ * @param newState
50
+ */
51
+ update(newState) {
52
+ const oldState = {...this._state};
53
+ Object.assign(this._state, newState);
54
+ this.element.dataset.tailmixState = JSON.stringify(this._state);
55
+ this.updater.run(this._state, oldState);
56
+ }
57
+
58
+ /**
59
+ * Dispatches a custom event with the specified name and detail.
60
+ * @param {string} name - The name of the event to be dispatched.
61
+ * @param {any} detail - The detail object to be attached to the event.
62
+ */
63
+ dispatch(name, detail) {
64
+ const event = new CustomEvent(`tailmix:${name}`, {bubbles: true, detail});
65
+ this.element.dispatchEvent(event);
66
+ }
67
+
68
+ /**
69
+ * Loads and parses the initial state from the data attribute.
70
+ * @return {Object} The parsed initial state.
71
+ */
72
+ loadInitialState() {
73
+ const initialState = JSON.parse(this.element.dataset.tailmixState || '{}');
74
+ const stateSchema = this.definition.states || {};
75
+
76
+ for (const key in stateSchema) {
77
+ if (initialState[key] === undefined && stateSchema[key].default !== undefined) {
78
+ initialState[key] = stateSchema[key].default;
79
+ }
80
+ }
81
+ return initialState;
82
+ }
83
+
84
+ /**
85
+ * Finds and retrieves all elements with a specific data attribute (`data-tailmix-element`)
86
+ * within the current root element, and maps their associated names to the DOM nodes.
87
+ * The root element itself is included if it also has the attribute with a defined name.
88
+ *
89
+ * @return {Object} An object where the keys are the names specified in the `data-tailmix-element`
90
+ * attribute, and the values are the corresponding DOM nodes.
91
+ */
92
+ findElements() {
93
+ const elements = {};
94
+ const elementNodes = this.element.querySelectorAll('[data-tailmix-element]');
95
+ elementNodes.forEach(node => {
96
+ const name = node.dataset.tailmixElement;
97
+ elements[name] = node;
98
+ });
99
+ // We also add the root element itself, if it has a name.
100
+ if (this.element.dataset.tailmixElement) {
101
+ elements[this.element.dataset.tailmixElement] = this.element;
102
+ }
103
+ return elements;
104
+ }
105
+
106
+ /**
107
+ * Initializes models by binding event listeners to elements and updating state accordingly.
108
+ * This method iterates through the component's elements and model bindings,
109
+ * and sets up event listeners for each attribute that needs to be updated.
110
+ *
111
+ * @return {void} This method does not return a value.
112
+ */
113
+ initializeModels() {
114
+ for (const elName in this.definition.elements) {
115
+ const element = this.elements[elName];
116
+ const modelBindings = this.definition.elements[elName].model_bindings;
117
+ if (!element || !modelBindings) continue;
118
+
119
+ for (const attrName in modelBindings) {
120
+ const binding = modelBindings[attrName];
121
+ element.addEventListener(binding.event, (event) => {
122
+ this.update({[binding.state]: event.target[attrName]});
123
+ if (binding.action) {
124
+ // It is possible to add action execution after model update.
125
+ }
126
+ });
127
+ }
128
+ }
129
+ }
130
+ }