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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +76 -145
  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 +39 -0
  10. data/examples/_modal_component.arb +17 -25
  11. data/examples/button.rb +81 -0
  12. data/examples/helpers.rb +25 -0
  13. data/examples/modal_component.rb +32 -164
  14. data/lib/tailmix/definition/builders/action_builder.rb +39 -0
  15. data/lib/tailmix/definition/{contexts → builders}/actions/element_builder.rb +1 -1
  16. data/lib/tailmix/definition/{contexts → builders}/attribute_builder.rb +1 -1
  17. data/lib/tailmix/definition/builders/component_builder.rb +81 -0
  18. data/lib/tailmix/definition/{contexts → builders}/dimension_builder.rb +2 -2
  19. data/lib/tailmix/definition/builders/element_builder.rb +83 -0
  20. data/lib/tailmix/definition/builders/reactor_builder.rb +43 -0
  21. data/lib/tailmix/definition/builders/rule_builder.rb +58 -0
  22. data/lib/tailmix/definition/builders/state_builder.rb +21 -0
  23. data/lib/tailmix/definition/{contexts → builders}/variant_builder.rb +17 -2
  24. data/lib/tailmix/definition/context_builder.rb +19 -13
  25. data/lib/tailmix/definition/merger.rb +2 -1
  26. data/lib/tailmix/definition/payload_proxy.rb +16 -0
  27. data/lib/tailmix/definition/result.rb +17 -18
  28. data/lib/tailmix/dev/docs.rb +32 -7
  29. data/lib/tailmix/dev/tools.rb +0 -5
  30. data/lib/tailmix/dsl.rb +3 -5
  31. data/lib/tailmix/engine.rb +11 -1
  32. data/lib/tailmix/html/attributes.rb +22 -12
  33. data/lib/tailmix/middleware/registry_cleaner.rb +17 -0
  34. data/lib/tailmix/registry.rb +37 -0
  35. data/lib/tailmix/runtime/action_proxy.rb +29 -0
  36. data/lib/tailmix/runtime/attribute_builder.rb +102 -0
  37. data/lib/tailmix/runtime/attribute_cache.rb +23 -0
  38. data/lib/tailmix/runtime/context.rb +51 -47
  39. data/lib/tailmix/runtime/facade_builder.rb +8 -5
  40. data/lib/tailmix/runtime/state.rb +36 -0
  41. data/lib/tailmix/runtime/state_proxy.rb +34 -0
  42. data/lib/tailmix/runtime.rb +0 -1
  43. data/lib/tailmix/version.rb +1 -1
  44. data/lib/tailmix/view_helpers.rb +49 -0
  45. data/lib/tailmix.rb +4 -2
  46. metadata +34 -21
  47. data/app/javascript/tailmix/finder.js +0 -15
  48. data/app/javascript/tailmix/index.js +0 -7
  49. data/app/javascript/tailmix/mutator.js +0 -28
  50. data/app/javascript/tailmix/runner.js +0 -7
  51. data/app/javascript/tailmix/stimulus_adapter.js +0 -37
  52. data/lib/tailmix/definition/contexts/action_builder.rb +0 -31
  53. data/lib/tailmix/definition/contexts/element_builder.rb +0 -41
  54. data/lib/tailmix/definition/contexts/stimulus_builder.rb +0 -101
  55. data/lib/tailmix/dev/stimulus_generator.rb +0 -124
  56. data/lib/tailmix/runtime/stimulus/compiler.rb +0 -59
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6eba946c8714d0699a6d9476c1c7b31f323418fdbf6fbc449bb96fb5ffe85f2
4
- data.tar.gz: 44460ef22c33497ff09002ef084691dafdedf19be942685faf8f578155f98351
3
+ metadata.gz: 25625758329e8524ce4fa37154be93d929b43ce85dfc5bb5669e8646b82ebd88
4
+ data.tar.gz: 1a98f9b38e7799819579309691e6692ec94ac5cf1358159c6c6431cc1d16c920
5
5
  SHA512:
6
- metadata.gz: 587453b63976416198477d8b38c02ed24e7c3d48972464fa926d2ee0cad42737f324dede5e3dac92d78e584ba71cfb6552b1be7f4eb94485816a598e9d3fe5c3
7
- data.tar.gz: 2d61fe96e8c2bf7ba9dc94876381bf0f5202dd0d22dd4aafe25ada2e977182b9693ae13d1f42c5d985980196ed1382587e9995fd5c614b925bab46f3159dec9f
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
@@ -1,214 +1,145 @@
1
1
  # Tailmix
2
2
 
3
- **Tailmix** is a powerful, declarative, and interactive CSS class manager for building maintainable UI components in Ruby. It's designed to work seamlessly with utility-first CSS frameworks like **Tailwind CSS**, allowing you to co-locate your style logic with your component's code—in a clean, structured, and highly reusable way.
3
+ **Tailmix** is a powerful, declarative engine for managing HTML attributes in Ruby UI components. It allows you to co-locate all presentational logic—including CSS classes, data attributes, and ARIA roles—directly within your component's code, creating truly self-contained and maintainable components.
4
4
 
5
- [![Gem Version](https://badge.fury.io/rb/tailmix.svg)](https://badge.fury.io/rb/tailmix)
6
- [![Build Status](https://github.com/alexander-s-f/tailmix/actions/workflows/main.yml/badge.svg)](https://github.com/alexander-s-f/tailmix/actions/workflows/main.yml)
5
+ [](https://badge.fury.io/rb/tailmix)
6
+ [](https://github.com/alexander-s-f/tailmix/actions/workflows/main.yml)
7
7
 
8
8
  Inspired by modern frontend tools like CVA (Class Variance Authority), Tailmix brings a robust styling engine to your server-side components (built with Arbre, ViewComponent, Phlex, etc.).
9
9
 
10
- ## Philosophy
10
+ ## Key Features
11
+
12
+ * **Declarative DSL:** Describe your component's styles with an elegant and intuitive API.
13
+ * **Variants & Compound Variants:** Easily manage different component states (`size`, `color`, etc.) and their combinations.
14
+ * **Component Inheritance:** Create base components and extend them to avoid code duplication.
15
+ * **Zero Dependencies:** Pure Ruby, ready to work in any project.
11
16
 
12
- * **Co-location & Isolation:** Define all style variants for a component directly within its class. No more hunting for styles in separate files. Each component is fully self-contained.
13
- * **Declarative First:** A beautiful DSL to declaratively describe your component's appearance based on variants like state, size, etc.
14
- * **Imperative Power:** A rich runtime API to dynamically add, remove, or toggle classes, perfect for server-side updates via Hotwire/Turbo.
15
- * **Framework-Agnostic:** Written in pure Ruby with zero dependencies, ready to be used in any project.
16
17
 
17
18
  ## Installation
18
19
 
19
20
  Add the gem to your Gemfile:
20
21
 
21
- ```ruby
22
- gem 'tailmix'
23
- ````
24
-
25
- Or install it from the command line:
26
-
27
22
  ```bash
28
23
  bundle add tailmix
29
24
  ```
30
25
 
31
- Next, run the installer to set up the JavaScript assets:
26
+ Then, run the installer to set up the JavaScript assets (required for `action` and `Stimulus` integration):
32
27
 
33
28
  ```bash
34
29
  bin/rails g tailmix:install
35
30
  ```
36
31
 
37
- ## Core Concepts
32
+ -----
38
33
 
39
- You define your component's appearance using a simple `tailmix do ... end` DSL inside your class.
34
+ ## Usage
40
35
 
41
- - `element :name, "base classes"`: Defines a logical part of your component (e.g., `:wrapper`, `:panel`, `:icon`).
42
- - `dimension :name, default: :value`: Defines a variant or "dimension" (e.g., `size` or `color`).
43
- - `variant :value, "classes"`: Defines the classes for a specific variant.
44
- - `action :name, method: :add | :toggle | :remove`: Defines a named set of UI mutations that can be applied on the server (`.apply!`) or passed to the client (`action_payload`).
45
- - `stimulus`: A powerful nested DSL for declaratively describing Stimulus `data-*` attributes.
36
+ The core idea of Tailmix is to describe all variants of your component within a Ruby class.
46
37
 
47
- ## Usage Example
38
+ ### 1. Basic Example: The `Modal` Component
48
39
 
49
- Let's build a complex `ModalComponent` from scratch.
50
- #### 1. Define the Component (`app/components/modal_component.rb`)
51
-
52
- This is a plain Ruby class that contains all the style and behavior logic.
40
+ **Component Definition:**
53
41
 
54
42
  ```ruby
43
+ # app/components/modal_component.rb
55
44
  class ModalComponent
56
45
  include Tailmix
57
46
  attr_reader :ui
58
47
 
59
48
  tailmix do
60
- element :base, "fixed inset-0 z-50 flex items-center justify-center" do
61
- dimension :open, default: false do
62
- variant true, "visible opacity-100"
49
+ plugin :auto_focus, on: :open_button, delay: 100
50
+ state :open, default: false, toggle: true
51
+
52
+ element :container do
53
+ end
54
+
55
+ element :open_button do
56
+ # We attach the `click` event to our auto-generated action.
57
+ on :click, :toggle_open
58
+ end
59
+
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"
63
63
  variant false, "invisible opacity-0"
64
64
  end
65
- stimulus.controller("modal")
66
65
  end
67
66
 
68
- element :overlay, "fixed inset-0 bg-black/50 transition-opacity" do
69
- stimulus.context("modal").action(click: :close)
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
70
73
  end
71
74
 
72
75
  element :panel, "relative bg-white rounded-lg shadow-xl" do
73
- dimension :size, default: :md do
74
- variant :sm, "w-full max-w-sm p-4"
75
- variant :md, "w-full max-w-md p-6"
76
+ dimension :open do
77
+ variant true, "block"
78
+ variant false, "hidden"
76
79
  end
77
80
  end
78
81
 
79
- element :close_button, "absolute top-2 right-2 p-1 text-gray-400" do
80
- stimulus.context("modal").action(click: :close)
82
+ element :close_button, "absolute top-2 right-2 p-1 text-gray-500 rounded-full cursor-pointer" do
83
+ on :click, :toggle_open
81
84
  end
82
85
 
83
- element :confirm_button, "px-4 py-2 bg-blue-600 text-white rounded-md" do
84
- stimulus.controller("form-submission")
85
- .action(:click, :show_pending_state)
86
- .action_payload(:show_pending_state, as: :pending_data)
87
- end
88
-
89
- action :show_pending_state, method: :add do
90
- element :confirm_button do
91
- classes "is-loading"
92
- data pending: true
93
- end
94
- end
86
+ element :title, "text-lg font-semibold text-gray-900 p-4 border-b"
87
+ element :body, "p-4 text-gray-900"
95
88
  end
96
89
 
97
- def initialize(open: false, size: :md)
98
- @ui = tailmix(open: open, size: size)
90
+ def initialize(open: false, id: nil)
91
+ @ui = tailmix(open: open, id: id)
99
92
  end
100
93
  end
101
94
  ```
102
95
 
103
- #### 2. Use in a View (Arbre, ERB, etc.)
104
-
105
- Thanks to Tailmix's design, you can pass `ui` objects directly to many rendering helpers.
106
-
107
- ##### Arbre
108
-
109
- The API is seamless. The `ui` object behaves like an attributes hash automatically.
110
-
111
- Ruby
112
-
113
- ```ruby
114
- # app/views/components/my_modal.arb
115
- # 1. Instantiate the component with the desired variants
116
- modal = ModalComponent.new(open: true, size: :sm)
96
+ **Usage in ERB:**
97
+ In ERB, use the `**` operator to pass the attributes.
117
98
 
118
- # 2. Render by passing the ui objects directly to Arbre's tag helpers
119
- div modal.ui.base do
120
- div modal.ui.overlay
99
+ ```erb
100
+ <% ui = ModalComponent.new(open: false, id: :user_profile_modal).ui %>
121
101
 
122
- div modal.ui.panel do
123
- # ... your content ...
124
- button modal.ui.confirm_button, "Confirm"
125
- end
126
- end
102
+ <div <%= tag.attributes **ui.container.component %>>
103
+ ...
104
+ </div>
127
105
  ```
128
106
 
129
- ##### ERB / Rails Tag Helpers
130
-
131
- In ERB, the idiomatic way to pass a hash-like object as attributes is with the double splat (`**`) operator.
132
-
133
- Фрагмент кода
134
-
135
- ```
136
- <%# app/views/pages/home.html.erb %>
137
- <% modal = ModalComponent.new(open: true, size: :sm) %>
138
-
139
- <%= tag.div **modal.ui.base do %>
140
- <%= tag.div **modal.ui.overlay %>
141
-
142
- <%= tag.div **modal.ui.panel do %>
143
- <%# ... your content ... %>
144
- <%= tag.button "Confirm", **modal.ui.confirm_button %>
145
- <% end %>
146
- <% end %>
147
- ```
148
-
149
- #### 3. Bring it to Life with Stimulus
150
-
151
- The `action_payload` helper makes it easy to connect server-side definitions with client-side behavior.
152
-
153
- JavaScript
154
-
155
- ```js
156
- // app/javascript/controllers/form_submission_controller.js
157
- import { Controller } from "@hotwired/stimulus"
158
- import Tailmix from "tailmix"
159
-
160
- export default class extends Controller {
161
- static values = { pendingData: Object }
162
-
163
- showPendingState(event) {
164
- event.preventDefault()
165
-
166
- // Instantly apply UI changes from the payload
167
- Tailmix.run({
168
- config: this.pendingDataValue,
169
- controller: this
170
- });
171
-
172
- // ... then submit the form or send an AJAX request
173
- }
174
- }
175
- ```
176
-
177
- ## Developer Tools
178
-
179
- Tailmix comes with built-in introspection tools to improve your development experience. Access them via the `.dev` namespace on your component class.
180
-
181
- #### Component Documentation
182
-
183
- Get a cheat sheet of all available `dimensions` and `actions` right in your console.
107
+ **Usage in Arbre:**
108
+ Arbre was the primary inspiration for Tailmix. Integration is seamless and does not require the `**` operator.
184
109
 
185
110
  ```ruby
186
- puts ModalComponent.dev.docs
187
- ```
111
+ # _example_modal_component.arb
112
+ modal_component = ModalComponent.new(open: false, id: :user_profile_modal)
113
+ ui = modal_component.ui
188
114
 
189
- #### Stimulus Controller Generator
115
+ button "Open Modal Outer", tailmix_trigger_for(:user_profile_modal, :toggle_open)
190
116
 
191
- Tailmix can analyze your component and scaffold a perfect boilerplate Stimulus controller with all targets, values, and action methods.
192
-
193
- ```ruby
194
- puts ModalComponent.dev.stimulus.scaffold("modal")
195
- ```
117
+ div ui.container.component do
118
+ button "Open Modal", ui.open_button
196
119
 
197
- ## Configuration
120
+ div ui.base do
121
+ div ui.overlay
198
122
 
199
- You can configure Tailmix by creating an initializer:
123
+ div ui.panel do
124
+ div ui.title do
125
+ h3 "Modal Title"
126
+ button ui.close_button do
127
+ text_node "✖"
128
+ end
129
+ end
200
130
 
201
- ```ruby
202
- # config/initializers/tailmix.rb
203
- Tailmix.configure do |config|
204
- # The attribute used by the universal JS selector.
205
- config.element_selector_attribute = "data-tm-el"
131
+ div ui.body do
132
+ para "This is the main content of the modal. It's powered by the new Tailmix Runtime!"
133
+ end
134
+ end
135
+ end
206
136
  end
207
137
  ```
208
138
 
139
+
209
140
  ## Contributing
210
141
 
211
- Bug reports and pull requests are welcome on GitHub at [https://github.com/alexander-s-f/tailmix](https://github.com/alexander-s-f/tailmix).
142
+ Bug reports and pull requests are welcome on GitHub.
212
143
 
213
144
  ## License
214
145
 
@@ -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
+ }