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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {Component} from './component';
|
|
2
|
+
import {PluginManager} from './plugins';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Tailmix global object that manages the lifecycle of components and plugins.
|
|
6
|
+
* It provides methods for starting the application, hydration of components,
|
|
7
|
+
*/
|
|
8
|
+
const Tailmix = {
|
|
9
|
+
_namedInstances: {},
|
|
10
|
+
_components: new Map(),
|
|
11
|
+
_definitions: {},
|
|
12
|
+
_pluginManager: new PluginManager(),
|
|
13
|
+
_observer: null,
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Starts the Tailmix application by loading component definitions and initializing components.
|
|
17
|
+
*/
|
|
18
|
+
start() {
|
|
19
|
+
this.loadDefinitions();
|
|
20
|
+
this._namedInstances = {};
|
|
21
|
+
this._components.clear();
|
|
22
|
+
|
|
23
|
+
this.hydrate(document.body);
|
|
24
|
+
this.observeChanges();
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hydrate components within the specified root element.
|
|
29
|
+
* @param rootElement
|
|
30
|
+
*/
|
|
31
|
+
hydrate(rootElement) {
|
|
32
|
+
const componentElements = rootElement.querySelectorAll('[data-tailmix-component]');
|
|
33
|
+
componentElements.forEach(element => {
|
|
34
|
+
if (this._components.has(element)) return;
|
|
35
|
+
|
|
36
|
+
const componentName = element.dataset.tailmixComponent;
|
|
37
|
+
const definition = this._definitions[componentName];
|
|
38
|
+
if (!definition) { /* ... */
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const component = new Component(element, definition, this._pluginManager);
|
|
43
|
+
this._components.set(element, component);
|
|
44
|
+
|
|
45
|
+
const componentId = element.dataset.tailmixId;
|
|
46
|
+
if (componentId) {
|
|
47
|
+
this._namedInstances[componentId] = component;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// External trigger binding
|
|
52
|
+
const triggerElements = rootElement.querySelectorAll('[data-tailmix-trigger-for]');
|
|
53
|
+
triggerElements.forEach(element => {
|
|
54
|
+
const targetId = element.dataset.tailmixTriggerFor;
|
|
55
|
+
const targetComponent = this._namedInstances[targetId];
|
|
56
|
+
if (targetComponent) {
|
|
57
|
+
targetComponent.dispatcher.bindAction(element);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Loads and parses the component definitions from a script tag with the attribute `data-tailmix-definitions`.
|
|
64
|
+
* If a valid JSON content is found, it assigns it to the `definitions` variable.
|
|
65
|
+
* Logs an error message to the console in case of parsing failure.
|
|
66
|
+
*
|
|
67
|
+
* @return {void} Does not return a value.
|
|
68
|
+
*/
|
|
69
|
+
loadDefinitions() {
|
|
70
|
+
const definitionsTag = document.querySelector('script[data-tailmix-definitions]');
|
|
71
|
+
if (definitionsTag) {
|
|
72
|
+
try {
|
|
73
|
+
this._definitions = JSON.parse(definitionsTag.textContent);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error("Tailmix: Failed to parse component definitions.", e);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Retrieves a component associated with the nearest ancestor element
|
|
82
|
+
* that has the `data-tailmix-component` attribute.
|
|
83
|
+
*
|
|
84
|
+
* @param {Element} element - The DOM element from which the component search begins.
|
|
85
|
+
* @return {*} The component associated with the identified ancestor element, or `undefined` if no component is found.
|
|
86
|
+
*/
|
|
87
|
+
getComponent(element) {
|
|
88
|
+
const root = element.closest('[data-tailmix-component]');
|
|
89
|
+
return root ? this._components.get(root) : undefined;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
registerPlugin(plugin) {
|
|
93
|
+
this._pluginManager.register(plugin);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Observes changes in the DOM and rehydrates components when they are added or modified.
|
|
98
|
+
*/
|
|
99
|
+
observeChanges() {
|
|
100
|
+
if (this._observer) this._observer.disconnect();
|
|
101
|
+
|
|
102
|
+
this._observer = new MutationObserver(mutations => {
|
|
103
|
+
for (const mutation of mutations) {
|
|
104
|
+
if (mutation.type === 'childList') {
|
|
105
|
+
mutation.addedNodes.forEach(node => {
|
|
106
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
107
|
+
// If the component itself was added
|
|
108
|
+
if (node.matches('[data-tailmix-component]')) {
|
|
109
|
+
this.hydrate(node);
|
|
110
|
+
}
|
|
111
|
+
// If a parent was added, which may contain components
|
|
112
|
+
this.hydrate(node);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this._observer.observe(document.body, {childList: true, subtree: true});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Initialize Tailmix on page load
|
|
124
|
+
document.addEventListener("turbo:load", () => {
|
|
125
|
+
console.log("Tailmix starting...");
|
|
126
|
+
Tailmix.start();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
export default Tailmix;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class PluginManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.plugins = [];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registers a plugin with the plugin manager.
|
|
8
|
+
* @param plugin
|
|
9
|
+
*/
|
|
10
|
+
register(plugin) {
|
|
11
|
+
if (!plugin.name) {
|
|
12
|
+
console.error("Tailmix Plugin Error: a plugin must have a name.", plugin);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
this.plugins.push(plugin);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connects a component to all registered plugins.
|
|
20
|
+
* @param component
|
|
21
|
+
*/
|
|
22
|
+
connect(component) {
|
|
23
|
+
this.plugins.forEach(plugin => {
|
|
24
|
+
if (typeof plugin.connect === 'function') {
|
|
25
|
+
const pluginConfig = component.definition.plugins?.[plugin.name];
|
|
26
|
+
if (pluginConfig) {
|
|
27
|
+
plugin.connect(component.api, pluginConfig);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export {PluginManager};
|
|
35
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export class Updater {
|
|
2
|
+
constructor(component) {
|
|
3
|
+
this.component = component;
|
|
4
|
+
this.definition = component.definition;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
run(newState, oldState) {
|
|
8
|
+
for (const elementName in this.definition.elements) {
|
|
9
|
+
const elementNode = this.component.elements[elementName];
|
|
10
|
+
const elementDef = this.definition.elements[elementName];
|
|
11
|
+
|
|
12
|
+
if (!elementNode || !elementDef) continue;
|
|
13
|
+
|
|
14
|
+
this.updateElement(elementNode, elementDef, newState, oldState);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
updateElement(elementNode, elementDef, newState, oldState) {
|
|
19
|
+
this.updateClasses(elementNode, elementDef, newState);
|
|
20
|
+
this.updateAttributes(elementNode, elementDef, newState);
|
|
21
|
+
this.updateContent(elementNode, elementDef, newState);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
updateClasses(elementNode, elementDef, newState) {
|
|
25
|
+
const targetClasses = this.calculateTargetClasses(elementDef, newState);
|
|
26
|
+
const currentClasses = new Set(elementNode.classList);
|
|
27
|
+
|
|
28
|
+
// We compare current classes with target classes and apply the difference.
|
|
29
|
+
targetClasses.forEach(cls => {
|
|
30
|
+
if (!currentClasses.has(cls)) {
|
|
31
|
+
elementNode.classList.add(cls);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
currentClasses.forEach(cls => {
|
|
36
|
+
if (!targetClasses.has(cls)) {
|
|
37
|
+
// We avoid removing base classes that were originally in HTML.
|
|
38
|
+
// (This is a simple heuristic; it can be improved if base classes are in the definition)
|
|
39
|
+
const isBaseClass = !this.isVariantClass(elementDef, cls);
|
|
40
|
+
if (!targetClasses.has(cls) && !isBaseClass) {
|
|
41
|
+
elementNode.classList.remove(cls);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
updateAttributes(elementNode, elementDef, newState) {
|
|
48
|
+
if (elementDef.attribute_bindings) {
|
|
49
|
+
for (const attrName in elementDef.attribute_bindings) {
|
|
50
|
+
if (["text", "html"].includes(attrName)) continue;
|
|
51
|
+
|
|
52
|
+
const stateKey = elementDef.attribute_bindings[attrName];
|
|
53
|
+
const newValue = newState[stateKey];
|
|
54
|
+
|
|
55
|
+
// We update the attribute only if it has changed.
|
|
56
|
+
if (elementNode.getAttribute(attrName) !== newValue) {
|
|
57
|
+
if (newValue === null || newValue === undefined) {
|
|
58
|
+
elementNode.removeAttribute(attrName);
|
|
59
|
+
} else {
|
|
60
|
+
elementNode.setAttribute(attrName, newValue);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
updateContent(elementNode, elementDef, newState) {
|
|
68
|
+
const bindings = elementDef.attribute_bindings;
|
|
69
|
+
if (!bindings) return;
|
|
70
|
+
|
|
71
|
+
// Обработка `bind :text`
|
|
72
|
+
const textStateKey = bindings.text;
|
|
73
|
+
if (textStateKey !== undefined) {
|
|
74
|
+
const newText = newState[textStateKey] || '';
|
|
75
|
+
if (elementNode.textContent !== newText) {
|
|
76
|
+
elementNode.textContent = newText;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Обработка `bind :html`
|
|
81
|
+
const htmlStateKey = bindings.html;
|
|
82
|
+
if (htmlStateKey !== undefined) {
|
|
83
|
+
const newHtml = newState[htmlStateKey] || '';
|
|
84
|
+
if (elementNode.innerHTML !== newHtml) {
|
|
85
|
+
elementNode.innerHTML = newHtml;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
calculateTargetClasses(elementDef, state) {
|
|
91
|
+
const classes = new Set();
|
|
92
|
+
|
|
93
|
+
// 1. We add base classes (if any are in the definition).
|
|
94
|
+
// (We skip this for now, as they are already in the HTML)
|
|
95
|
+
|
|
96
|
+
// 2. We apply classes from active variants (dimensions).
|
|
97
|
+
if (elementDef.dimensions) {
|
|
98
|
+
for (const dimName in elementDef.dimensions) {
|
|
99
|
+
const dimDef = elementDef.dimensions[dimName];
|
|
100
|
+
const stateValue = state[dimName] !== undefined ? state[dimName] : dimDef.default;
|
|
101
|
+
|
|
102
|
+
const variantDef = dimDef.variants?.[stateValue];
|
|
103
|
+
if (variantDef?.classes) {
|
|
104
|
+
variantDef.classes.forEach(cls => classes.add(cls));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. We apply classes from active compound variants.
|
|
110
|
+
if (elementDef.compound_variants) {
|
|
111
|
+
elementDef.compound_variants.forEach(cv => {
|
|
112
|
+
const conditions = cv.on;
|
|
113
|
+
const modifications = cv.modifications;
|
|
114
|
+
|
|
115
|
+
const isMatch = Object.entries(conditions).every(([key, value]) => {
|
|
116
|
+
return state[key] === value;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (isMatch && modifications.classes) {
|
|
120
|
+
modifications.classes.forEach(cls => classes.add(cls));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return classes;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isVariantClass(elementDef, className) {
|
|
129
|
+
if (elementDef.dimensions) {
|
|
130
|
+
for (const dimName in elementDef.dimensions) {
|
|
131
|
+
const dim = elementDef.dimensions[dimName];
|
|
132
|
+
for (const variantName in dim.variants) {
|
|
133
|
+
if (dim.variants[variantName].classes?.includes(className)) return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ... a check can also be added for compound_variants
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
data/docs/01_getting_started.md
CHANGED
|
@@ -34,87 +34,6 @@ This command will add tailmix to your importmap.rb and ensure its JavaScript is
|
|
|
34
34
|
Let's create a simple BadgeComponent to see Tailmix in action.
|
|
35
35
|
|
|
36
36
|
#### 1. Define the Component Class
|
|
37
|
-
|
|
38
|
-
Create a new file in `app/components/badge_component.rb`:
|
|
39
|
-
|
|
40
|
-
```ruby
|
|
41
|
-
# app/components/badge_component.rb
|
|
42
|
-
class BadgeComponent
|
|
43
|
-
include Tailmix
|
|
44
|
-
attr_reader :ui, :text
|
|
45
|
-
|
|
46
|
-
def initialize(text, color: :gray)
|
|
47
|
-
@ui = tailmix(color: color)
|
|
48
|
-
@text = text
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
tailmix do
|
|
52
|
-
element :badge, "inline-flex items-center px-2.5 py-0.5 text-xs font-medium rounded-full" do
|
|
53
|
-
dimension :color, default: :gray do
|
|
54
|
-
variant :gray, "bg-gray-100 text-gray-600"
|
|
55
|
-
variant :success, "bg-green-100 text-green-700"
|
|
56
|
-
variant :danger, "bg-red-100 text-red-700"
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
#### 2. Use it in a View
|
|
64
|
-
|
|
65
|
-
Now, you can use this component in any ERB view:
|
|
66
|
-
|
|
67
|
-
```html
|
|
68
|
-
<%# Create two instances of our badge with different variants %>
|
|
69
|
-
<% success_badge = BadgeComponent.new("Active", color: :success) %>
|
|
70
|
-
<% danger_badge = BadgeComponent.new("Inactive", color: :danger) %>
|
|
71
|
-
|
|
72
|
-
<span <%= tag.attributes **success_badge.ui.badge %>>
|
|
73
|
-
<%= success_badge.text %>
|
|
74
|
-
</span>
|
|
75
|
-
|
|
76
|
-
<span <%= tag.attributes **danger_badge.ui.badge %>>
|
|
77
|
-
<%= danger_badge.text %>
|
|
78
|
-
</span>
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
#### View Usage .arb (Ruby Arbre)
|
|
82
|
-
|
|
83
|
-
```ruby
|
|
84
|
-
# app/views/components/_badge.arb
|
|
85
|
-
success_badge = BadgeComponent.new("Active", color: :success)
|
|
86
|
-
danger_badge = BadgeComponent.new("Inactive", color: :danger)
|
|
87
|
-
|
|
88
|
-
span success_badge.ui.badge do
|
|
89
|
-
success_badge.text
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
span danger_badge.ui.badge do
|
|
93
|
-
danger_badge.text
|
|
94
|
-
end
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
#### 3. The Resulting HTML
|
|
98
|
-
|
|
99
|
-
This will produce the following clean and semantic HTML:
|
|
100
|
-
|
|
101
|
-
```html
|
|
102
|
-
<span class="inline-flex ... bg-green-100 text-green-700" data-tailmix-badge="color:success">
|
|
103
|
-
Active
|
|
104
|
-
</span>
|
|
105
|
-
|
|
106
|
-
<span class="inline-flex ... bg-red-100 text-red-700" data-tailmix-badge="color:danger">
|
|
107
|
-
Inactive
|
|
108
|
-
</span>
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Notice the `data-tailmix-badge` attribute, which serves as both a stable selector and a state indicator for your component.
|
|
112
|
-
|
|
113
|
-
## Next Steps
|
|
114
|
-
|
|
115
|
-
- You've successfully created your first component! To learn more about the power of Tailmix, check out these documents:
|
|
116
37
|
|
|
117
|
-
- DSL Reference: For a deep dive into every available DSL method.
|
|
118
38
|
|
|
119
|
-
- Cookbook: For practical, real-world examples of common UI components.
|
|
120
39
|
|
|
@@ -1,36 +1,28 @@
|
|
|
1
1
|
# Arbre view:
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
modal_component = ModalComponent.new(open: false, id: :user_profile_modal)
|
|
4
|
+
ui = modal_component.ui
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
ui = modal.ui
|
|
6
|
+
button "Open Modal Outer", tailmix_trigger_for(:user_profile_modal, :toggle_open)
|
|
7
7
|
|
|
8
|
-
div ui.
|
|
9
|
-
|
|
10
|
-
div ui.open do
|
|
11
|
-
"open"
|
|
12
|
-
end
|
|
8
|
+
div ui.container.component do
|
|
9
|
+
button "Open Modal", ui.open_button
|
|
13
10
|
|
|
14
|
-
div ui.
|
|
15
|
-
|
|
16
|
-
span "Close"
|
|
17
|
-
end
|
|
11
|
+
div ui.base do
|
|
12
|
+
div ui.overlay
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"Your payment has been successfully submitted. We’ve sent you an email with all of the details of your order."
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
div ui.footer do
|
|
28
|
-
button ui.confirm_button do
|
|
29
|
-
span "Confirm Purchase"
|
|
30
|
-
div ui.spinner do
|
|
31
|
-
span "Loading..."
|
|
14
|
+
div ui.panel do
|
|
15
|
+
div ui.title do
|
|
16
|
+
h3 "Modal Title"
|
|
17
|
+
button ui.close_button do
|
|
18
|
+
text_node "✖"
|
|
32
19
|
end
|
|
33
20
|
end
|
|
21
|
+
|
|
22
|
+
div ui.body do
|
|
23
|
+
para "This is the main content of the modal. It's powered by the new Tailmix Runtime!"
|
|
24
|
+
end
|
|
34
25
|
end
|
|
35
26
|
end
|
|
36
27
|
end
|
|
28
|
+
|
data/examples/modal_component.rb
CHANGED
|
@@ -8,186 +8,62 @@ class ModalComponent
|
|
|
8
8
|
attr_reader :ui
|
|
9
9
|
|
|
10
10
|
tailmix do
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
variant true, "visible opacity-100"
|
|
14
|
-
variant false, "invisible opacity-0"
|
|
15
|
-
end
|
|
16
|
-
stimulus.controller("modal").action_payload(:toggle, as: :toggle_data)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
element :overlay, "fixed inset-0 bg-black/50 transition-opacity" do
|
|
20
|
-
stimulus.context("modal").action(:click, :close)
|
|
21
|
-
end
|
|
11
|
+
plugin :auto_focus, on: :open_button, delay: 100
|
|
12
|
+
state :open, default: false, toggle: true
|
|
22
13
|
|
|
23
|
-
element :
|
|
24
|
-
dimension :size, default: :md do
|
|
25
|
-
variant :sm, "w-full max-w-sm p-4" do
|
|
26
|
-
classes "dark:text-slate-400", group: :dark_mode
|
|
27
|
-
classes "one two"
|
|
28
|
-
end
|
|
29
|
-
variant :md, "w-full max-w-md p-6"
|
|
30
|
-
variant :lg, "w-full max-w-lg p-8"
|
|
31
|
-
end
|
|
32
|
-
stimulus.context("modal").target("panel")
|
|
14
|
+
element :container do
|
|
33
15
|
end
|
|
34
16
|
|
|
35
|
-
element :
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
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
|
|
39
20
|
end
|
|
40
21
|
|
|
41
|
-
element :
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
27
|
end
|
|
47
28
|
|
|
48
|
-
element :
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
classes "hidden"
|
|
53
|
-
end
|
|
54
|
-
element :panel do
|
|
55
|
-
classes "hidden"
|
|
29
|
+
element :overlay do
|
|
30
|
+
dimension :open do
|
|
31
|
+
variant true, "fixed inset-0 bg-black/50"
|
|
32
|
+
variant false, "hidden"
|
|
56
33
|
end
|
|
34
|
+
on :click, :toggle_open
|
|
57
35
|
end
|
|
58
36
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
element :panel do
|
|
64
|
-
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"
|
|
65
41
|
end
|
|
66
42
|
end
|
|
67
43
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
classes "opacity-75 cursor-not-allowed"
|
|
71
|
-
end
|
|
72
|
-
element :spinner do
|
|
73
|
-
classes "flex"
|
|
74
|
-
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
|
|
75
46
|
end
|
|
76
|
-
end
|
|
77
47
|
|
|
78
|
-
|
|
79
|
-
|
|
48
|
+
element :title, "text-lg font-semibold text-gray-900 p-4 border-b"
|
|
49
|
+
element :body, "p-4 text-gray-900"
|
|
80
50
|
end
|
|
81
51
|
|
|
82
|
-
def
|
|
83
|
-
@ui
|
|
52
|
+
def initialize(open: false, id: nil)
|
|
53
|
+
@ui = tailmix(open: open, id: id)
|
|
84
54
|
end
|
|
85
55
|
end
|
|
86
56
|
|
|
87
|
-
puts "-" * 100
|
|
88
|
-
# puts ModalComponent.dev.docs
|
|
89
|
-
# puts ""
|
|
90
|
-
# puts "Scaffolds:"
|
|
91
|
-
# puts ""
|
|
92
|
-
# puts ModalComponent.dev.stimulus.scaffold
|
|
93
|
-
# puts ""
|
|
94
|
-
|
|
95
|
-
# >>>
|
|
96
|
-
#
|
|
97
|
-
# == Tailmix Docs for ModalComponent ==
|
|
98
|
-
# Signature: `initialize(open: true, size: :md)`
|
|
99
|
-
#
|
|
100
|
-
# Dimensions:
|
|
101
|
-
# - open (default: true)
|
|
102
|
-
# - true:
|
|
103
|
-
# - classes : "visible opacity-100"
|
|
104
|
-
# - false:
|
|
105
|
-
# - classes : "invisible opacity-0"
|
|
106
|
-
# - size (default: :md)
|
|
107
|
-
# - :sm:
|
|
108
|
-
# - classes : "w-full max-w-sm p-4"
|
|
109
|
-
# - classes (group: :dark_mode): "dark:text-slate-400"
|
|
110
|
-
# - classes : "one two"
|
|
111
|
-
# - :md:
|
|
112
|
-
# - classes : "w-full max-w-md p-6"
|
|
113
|
-
# - :lg:
|
|
114
|
-
# - classes : "w-full max-w-lg p-8"
|
|
115
|
-
#
|
|
116
|
-
# Actions:
|
|
117
|
-
# - :toggle
|
|
118
|
-
# - :lock
|
|
119
|
-
# - :enter_pending_state
|
|
120
|
-
#
|
|
121
|
-
# Stimulus:
|
|
122
|
-
# - on `modal` controller:
|
|
123
|
-
# - Targets: panel
|
|
124
|
-
# - Actions: close
|
|
125
|
-
#
|
|
126
|
-
# Stimulus:
|
|
127
|
-
# - on `form-submission` controller:
|
|
128
|
-
# - Actions: submit
|
|
129
|
-
#
|
|
130
|
-
# Scaffolds:
|
|
131
|
-
#
|
|
132
|
-
# // Generated by Tailmix for the "modal" controller
|
|
133
|
-
# // Path: app/javascript/controllers/modal_controller.js
|
|
134
|
-
# import { Controller } from "@hotwired/stimulus"
|
|
135
|
-
# import Tailmix from "tailmix"
|
|
136
|
-
#
|
|
137
|
-
# export default class extends Controller {
|
|
138
|
-
# static targets = ['panel']
|
|
139
|
-
# static values = { toggleData: Object }
|
|
140
|
-
#
|
|
141
|
-
# connect() {
|
|
142
|
-
# console.log("modal controller connected to", this.element);
|
|
143
|
-
# }
|
|
144
|
-
# toggle(event) {
|
|
145
|
-
# if (event) event.preventDefault();
|
|
146
|
-
# Tailmix.run({ config: this.toggleDataValue, controller: this });
|
|
147
|
-
# }
|
|
148
|
-
#
|
|
149
|
-
# close() {
|
|
150
|
-
# console.log('modal#close fired');
|
|
151
|
-
# }
|
|
152
|
-
# }
|
|
153
|
-
# ------------------------------------------------------------
|
|
154
|
-
#
|
|
155
|
-
# // Generated by Tailmix for the "form-submission" controller
|
|
156
|
-
# // Path: 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 targets = []
|
|
162
|
-
# static values = { pendingData: Object }
|
|
163
|
-
#
|
|
164
|
-
# connect() {
|
|
165
|
-
# console.log("form-submission controller connected to", this.element);
|
|
166
|
-
# }
|
|
167
|
-
# enterPendingState(event) {
|
|
168
|
-
# if (event) event.preventDefault();
|
|
169
|
-
# Tailmix.run({ config: this.pendingDataValue, controller: this });
|
|
170
|
-
# }
|
|
171
|
-
#
|
|
172
|
-
# submit() {
|
|
173
|
-
# console.log('form-submission#submit fired');
|
|
174
|
-
# }
|
|
175
|
-
# }
|
|
176
|
-
# ------------------------------------------------------------
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
modal = ModalComponent.new(size: :lg, open: true)
|
|
182
|
-
# modal.lock!
|
|
183
|
-
ui = modal.ui
|
|
184
|
-
|
|
185
57
|
|
|
58
|
+
modal = ModalComponent.new(open: false, id: :user_profile_modal)
|
|
59
|
+
ui = modal.ui
|
|
186
60
|
|
|
187
61
|
|
|
188
62
|
# puts "Definition:"
|
|
189
63
|
# puts JSON.pretty_generate(stringify_keys(ModalComponent.tailmix_definition.to_h))
|
|
190
|
-
|
|
64
|
+
puts "-" * 100
|
|
65
|
+
puts ModalComponent.dev.docs
|
|
66
|
+
puts "-" * 100
|
|
191
67
|
|
|
192
68
|
ModalComponent.dev.elements.each do |element_name|
|
|
193
69
|
element = ui.send(element_name)
|