@ramstack/alpinegear-format 1.0.0-preview.1
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.
- package/README.md +145 -0
- package/alpinegear-format.esm.js +100 -0
- package/alpinegear-format.esm.min.js +1 -0
- package/alpinegear-format.js +105 -0
- package/alpinegear-format.min.js +1 -0
- package/package.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# @ramstack/alpinegear-format
|
|
2
|
+
|
|
3
|
+
`@ramstack/alpinegear-format` is a plugin for [Alpine.js](https://alpinejs.dev/) that provides the `x-format` directive.
|
|
4
|
+
|
|
5
|
+
This directive allows you to easily interpolate text using a template syntax similar to what's available in `Vue.js`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### Using CDN
|
|
10
|
+
To include the CDN version of this plugin, add the following `<script>` tag before the core `alpine.js` file:
|
|
11
|
+
|
|
12
|
+
```html
|
|
13
|
+
<!-- alpine.js plugin -->
|
|
14
|
+
<script src="https://cdn.jsdelivr.net/npm/@ramstack/alpinegear-format@1/alpinegear-format.min.js" defer></script>
|
|
15
|
+
|
|
16
|
+
<!-- alpine.js -->
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Using NPM
|
|
21
|
+
Alternatively, you can install the plugin via `npm`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install --save @ramstack/alpinegear-format
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then initialize it in your bundle:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import Alpine from "alpinejs";
|
|
31
|
+
import format from "@ramstack/alpinegear-format";
|
|
32
|
+
|
|
33
|
+
Alpine.plugin(format);
|
|
34
|
+
Alpine.start();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
The `x-format` directive enables you to use double curly braces (`{{ ... }}`) to evaluate expressions
|
|
39
|
+
and inject their values into the DOM.
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<div x-data="{ message: 'Hello, World!'}" x-format>
|
|
43
|
+
<span>Message: {{ message }}</span>
|
|
44
|
+
</div>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
In this example, `{{ message }}` will be replaced by the value of the `message` property,
|
|
48
|
+
and the content will update whenever the `message` property changes.
|
|
49
|
+
|
|
50
|
+
### Using with Attributes
|
|
51
|
+
The `x-format` directive can also be used to interpolate values inside HTML attributes:
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<div x-data="{ message: 'Hello, World!'}" x-format>
|
|
55
|
+
<span title="Message: {{ message }}">
|
|
56
|
+
{{ message }}
|
|
57
|
+
<label>
|
|
58
|
+
Message: <input x-model="message" />
|
|
59
|
+
</label>
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Just like with text interpolation, the attribute values will be updated automatically when the data changes.
|
|
65
|
+
|
|
66
|
+
> [!WARNING]
|
|
67
|
+
> Keep in mind that interpolation within a `<textarea>` element may not work as you expect.
|
|
68
|
+
>
|
|
69
|
+
> Use `x-model` instead.
|
|
70
|
+
|
|
71
|
+
### Using `once` modifier
|
|
72
|
+
The `once` modifier allows you to interpolate the template only once.
|
|
73
|
+
After the initial rendering, the content remains static and will not update, even if the data changes.
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<div x-data="{ message: 'Hello, World!'}" x-format.once>
|
|
77
|
+
<span title="Message: {{ message }}">
|
|
78
|
+
{{ message }}
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> [!IMPORTANT]
|
|
84
|
+
> By default, `x-format` treats the interpolated values as plain text, not HTML.
|
|
85
|
+
>
|
|
86
|
+
> If you need to render HTML, you should use the `x-html` directive instead.
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
## Optimization
|
|
90
|
+
The `x-format` directive is optimized to update only the parts of the text that change,
|
|
91
|
+
without replacing the entire DOM element. This is especially useful for large or complex DOM structures.
|
|
92
|
+
|
|
93
|
+
For example:
|
|
94
|
+
```html
|
|
95
|
+
<div x-data="{ message: 'Hello, World!'}" x-format>
|
|
96
|
+
The 'message' value is '{{ message }}' and it updates when the property changes.
|
|
97
|
+
</div>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
In this case, the text will be split into three separate text nodes:
|
|
101
|
+
1. `The 'message' value is '`
|
|
102
|
+
2. `{{ message }}`
|
|
103
|
+
3. `' and it updates when the property changes.`
|
|
104
|
+
|
|
105
|
+
Only the `{{ message }}` text node will be updated, while the static nodes will remain unchanged.
|
|
106
|
+
|
|
107
|
+
> [!NOTE]
|
|
108
|
+
> This optimization does not apply to attribute values.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
## Dynamic Elements
|
|
112
|
+
Since the `x-format` directive doesn't automatically track changes to the DOM,
|
|
113
|
+
newly added elements (e.g., via `x-if` or `x-for`) will not automatically interpolate their templates.
|
|
114
|
+
|
|
115
|
+
For instance, in the example below, the `{{ message }}` inside `x-if` remains unchanged:
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<div x-data="{ show: false, message: 'Hello, World!'}" x-format>
|
|
119
|
+
<template x-if="show">
|
|
120
|
+
<span>{{ message }}</span>
|
|
121
|
+
</template>
|
|
122
|
+
</div>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
To ensure proper interpolation, include the `x-format` directive in the dynamically rendered elements:
|
|
126
|
+
|
|
127
|
+
```html
|
|
128
|
+
<div x-data="{ show: false, message: 'Hello, World!'}">
|
|
129
|
+
<template x-if="show">
|
|
130
|
+
<span x-format>{{ message }}</span>
|
|
131
|
+
</template>
|
|
132
|
+
</div>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Source Code
|
|
136
|
+
You can find the source code for this plugin on GitHub:
|
|
137
|
+
|
|
138
|
+
https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/format
|
|
139
|
+
|
|
140
|
+
## Contributions
|
|
141
|
+
Bug reports and contributions are welcome.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
This package is released as open source under the **MIT License**.
|
|
145
|
+
See the [LICENSE](https://github.com/rameel/ramstack.alpinegear.js/blob/main/LICENSE) file for more details.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
function create_getter(evaluate_later, ...args) {
|
|
2
|
+
const evaluate = evaluate_later(...args);
|
|
3
|
+
return () => {
|
|
4
|
+
let result;
|
|
5
|
+
evaluate(v => result = v);
|
|
6
|
+
return has_getter(result) ? result.get() : result;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function has_getter(value) {
|
|
11
|
+
return typeof value?.get === "function";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const is_nullish = value => value === null || value === undefined;
|
|
15
|
+
const has_modifier = (modifiers, modifier) => modifiers.includes(modifier);
|
|
16
|
+
|
|
17
|
+
function plugin({ directive, mutateDom }) {
|
|
18
|
+
directive("format", (el, { modifiers }, { effect, evaluateLater }) => {
|
|
19
|
+
const cache = new Map;
|
|
20
|
+
const placeholder_regex = /{{(?<expr>.+?)}}/g;
|
|
21
|
+
const is_once = has_modifier(modifiers, "once");
|
|
22
|
+
|
|
23
|
+
process(el);
|
|
24
|
+
|
|
25
|
+
function get_eval_fn(expression) {
|
|
26
|
+
let getter = cache.get(expression);
|
|
27
|
+
if (is_nullish(getter)) {
|
|
28
|
+
getter = create_getter(evaluateLater, expression);
|
|
29
|
+
cache.set(expression, getter);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return getter;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function update(callback) {
|
|
36
|
+
if (is_once) {
|
|
37
|
+
mutateDom(() => callback());
|
|
38
|
+
cache.clear();
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
effect(() => mutateDom(() => callback()));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function process(node) {
|
|
46
|
+
switch (node.nodeType) {
|
|
47
|
+
case Node.TEXT_NODE:
|
|
48
|
+
process_text_node(node);
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case Node.ELEMENT_NODE:
|
|
52
|
+
process_nodes(node);
|
|
53
|
+
process_attributes(node);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function process_text_node(node) {
|
|
59
|
+
const tokens = node.textContent.split(placeholder_regex);
|
|
60
|
+
|
|
61
|
+
if (tokens.length > 1) {
|
|
62
|
+
const fragment = new DocumentFragment();
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
65
|
+
if ((i % 2) === 0) {
|
|
66
|
+
fragment.appendChild(document.createTextNode(tokens[i]));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const get_value = get_eval_fn(tokens[i]);
|
|
70
|
+
const text = document.createTextNode("");
|
|
71
|
+
|
|
72
|
+
fragment.append(text);
|
|
73
|
+
update(() => text.textContent = get_value());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
mutateDom(() =>
|
|
78
|
+
node.parentElement.replaceChild(fragment, node));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function process_attributes(node) {
|
|
83
|
+
for (let attr of node.attributes) {
|
|
84
|
+
const matches = [...attr.value.matchAll(placeholder_regex)];
|
|
85
|
+
if (matches.length) {
|
|
86
|
+
const template = attr.value;
|
|
87
|
+
update(() => attr.value = template.replace(placeholder_regex, (_, expr) => get_eval_fn(expr)()));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function process_nodes(node) {
|
|
93
|
+
for (let child of node.childNodes) {
|
|
94
|
+
process(child);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { plugin as format };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e({directive:e,mutateDom:t}){e("format",((e,{modifiers:n},{effect:o,evaluateLater:c})=>{const a=new Map,l=/{{(?<expr>.+?)}}/g,r=(e=>e.includes("once"))(n);function f(e){let t=a.get(e);return null==t&&(t=function(e,...t){const n=e(...t);return()=>{let e;return n((t=>e=t)),t=e,"function"==typeof t?.get?e.get():e;var t}}(c,e),a.set(e,t)),t}function i(e){r?(t((()=>e())),a.clear()):o((()=>t((()=>e()))))}!function e(n){switch(n.nodeType){case Node.TEXT_NODE:!function(e){const n=e.textContent.split(l);if(n.length>1){const o=new DocumentFragment;for(let e=0;n.length>e;e++)if(e%2==0)o.appendChild(document.createTextNode(n[e]));else{const t=f(n[e]),c=document.createTextNode("");o.append(c),i((()=>c.textContent=t()))}t((()=>e.parentElement.replaceChild(o,e)))}}(n);break;case Node.ELEMENT_NODE:!function(t){for(let n of t.childNodes)e(n)}(n),function(e){for(let t of e.attributes)if([...t.value.matchAll(l)].length){const e=t.value;i((()=>t.value=e.replace(l,((e,t)=>f(t)()))))}}(n)}}(e)}))}export{e as format};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
function create_getter(evaluate_later, ...args) {
|
|
5
|
+
const evaluate = evaluate_later(...args);
|
|
6
|
+
return () => {
|
|
7
|
+
let result;
|
|
8
|
+
evaluate(v => result = v);
|
|
9
|
+
return has_getter(result) ? result.get() : result;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function has_getter(value) {
|
|
14
|
+
return typeof value?.get === "function";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const is_nullish = value => value === null || value === undefined;
|
|
18
|
+
const has_modifier = (modifiers, modifier) => modifiers.includes(modifier);
|
|
19
|
+
|
|
20
|
+
function plugin({ directive, mutateDom }) {
|
|
21
|
+
directive("format", (el, { modifiers }, { effect, evaluateLater }) => {
|
|
22
|
+
const cache = new Map;
|
|
23
|
+
const placeholder_regex = /{{(?<expr>.+?)}}/g;
|
|
24
|
+
const is_once = has_modifier(modifiers, "once");
|
|
25
|
+
|
|
26
|
+
process(el);
|
|
27
|
+
|
|
28
|
+
function get_eval_fn(expression) {
|
|
29
|
+
let getter = cache.get(expression);
|
|
30
|
+
if (is_nullish(getter)) {
|
|
31
|
+
getter = create_getter(evaluateLater, expression);
|
|
32
|
+
cache.set(expression, getter);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return getter;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function update(callback) {
|
|
39
|
+
if (is_once) {
|
|
40
|
+
mutateDom(() => callback());
|
|
41
|
+
cache.clear();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
effect(() => mutateDom(() => callback()));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function process(node) {
|
|
49
|
+
switch (node.nodeType) {
|
|
50
|
+
case Node.TEXT_NODE:
|
|
51
|
+
process_text_node(node);
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case Node.ELEMENT_NODE:
|
|
55
|
+
process_nodes(node);
|
|
56
|
+
process_attributes(node);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function process_text_node(node) {
|
|
62
|
+
const tokens = node.textContent.split(placeholder_regex);
|
|
63
|
+
|
|
64
|
+
if (tokens.length > 1) {
|
|
65
|
+
const fragment = new DocumentFragment();
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
68
|
+
if ((i % 2) === 0) {
|
|
69
|
+
fragment.appendChild(document.createTextNode(tokens[i]));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const get_value = get_eval_fn(tokens[i]);
|
|
73
|
+
const text = document.createTextNode("");
|
|
74
|
+
|
|
75
|
+
fragment.append(text);
|
|
76
|
+
update(() => text.textContent = get_value());
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
mutateDom(() =>
|
|
81
|
+
node.parentElement.replaceChild(fragment, node));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function process_attributes(node) {
|
|
86
|
+
for (let attr of node.attributes) {
|
|
87
|
+
const matches = [...attr.value.matchAll(placeholder_regex)];
|
|
88
|
+
if (matches.length) {
|
|
89
|
+
const template = attr.value;
|
|
90
|
+
update(() => attr.value = template.replace(placeholder_regex, (_, expr) => get_eval_fn(expr)()));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function process_nodes(node) {
|
|
96
|
+
for (let child of node.childNodes) {
|
|
97
|
+
process(child);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
document.addEventListener("alpine:init", () => { Alpine.plugin(plugin); });
|
|
104
|
+
|
|
105
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(){"use strict";function e({directive:e,mutateDom:t}){e("format",((e,{modifiers:n},{effect:o,evaluateLater:c})=>{const i=new Map,l=/{{(?<expr>.+?)}}/g,u=(e=>e.includes("once"))(n);function a(e){let t=i.get(e);return null==t&&(t=function(e,...t){const n=e(...t);return()=>{let e;return n((t=>e=t)),t=e,"function"==typeof t?.get?e.get():e;var t}}(c,e),i.set(e,t)),t}function r(e){u?(t((()=>e())),i.clear()):o((()=>t((()=>e()))))}!function e(n){switch(n.nodeType){case Node.TEXT_NODE:!function(e){const n=e.textContent.split(l);if(n.length>1){const o=new DocumentFragment;for(let e=0;n.length>e;e++)if(e%2==0)o.appendChild(document.createTextNode(n[e]));else{const t=a(n[e]),c=document.createTextNode("");o.append(c),r((()=>c.textContent=t()))}t((()=>e.parentElement.replaceChild(o,e)))}}(n);break;case Node.ELEMENT_NODE:!function(t){for(let n of t.childNodes)e(n)}(n),function(e){for(let t of e.attributes)if([...t.value.matchAll(l)].length){const e=t.value;r((()=>t.value=e.replace(l,((e,t)=>a(t)()))))}}(n)}}(e)}))}document.addEventListener("alpine:init",(()=>{Alpine.plugin(e)}))}();
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ramstack/alpinegear-format",
|
|
3
|
+
"version": "1.0.0-preview.1",
|
|
4
|
+
"description": "@ramstack/alpinegear-format provides 'x-format' Alpine.js directive, which allows you to easily interpolate text using a template syntax similar to what's available in Vue.js.",
|
|
5
|
+
"author": "Rameel Burhan",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/rameel/ramstack.alpinegear.js.git",
|
|
10
|
+
"directory": "src/plugins/format"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"alpine.js",
|
|
14
|
+
"alpinejs"
|
|
15
|
+
],
|
|
16
|
+
"main": "alpinegear-format.js",
|
|
17
|
+
"module": "alpinegear-format.esm.js"
|
|
18
|
+
}
|