Skip to content

Commit c16b038

Browse files
authored
Next 0.15.0 (#11)
* further remove the need to include the attr. for conditional arttributes * only unmount if rendered target * simplify atrribute handling
1 parent 817a7a6 commit c16b038

File tree

5 files changed

+134
-81
lines changed

5 files changed

+134
-81
lines changed

docs-src/documentation/conditional-attributes.page.ts

Lines changed: 25 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default ({
2020
content: html`
2121
${Heading(page.name)}
2222
<p>
23-
You cannot use template literal value to define attributes directly on the
23+
Markup does not allow you to use template literal value to define attributes directly on the
2424
tag.
2525
</p>
2626
${CodeSnippet(
@@ -30,49 +30,33 @@ export default ({
3030
'// renders <button>click me</button>',
3131
'typescript'
3232
)}
33-
<p>
34-
This means that you need another way to dynamically render
35-
attributes and that way is the Markup <code>attr</code> attribute's name prefixer.
36-
</p>
37-
${CodeSnippet(
38-
'const disabled = true;\n' +
39-
'\n' +
40-
'html`<button attr.disabled="${disabled}">click me</button>`;',
41-
'typescript'
42-
)}
43-
<p>
44-
In the above example the <code>disabled</code> attribute
45-
was prefixed with <code>attr.</code> then provided the condition(boolean) as
46-
value to whether include that attribute.
47-
</p>
48-
<div class="info">
49-
The <code>attr.</code> is not always needed. Attributes like <code>class</code>, <code>style</code>, <code>data</code>,
50-
and <a href="https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML" target="_blank">HTML boolean
51-
attributes</a>
52-
work without it or can have the condition specified after the pipe <code>|</code> as the value. Everything
53-
else requires it.
54-
</div>
33+
<p>There is a different way to go about conditionally set attributes.</p>
5534
${Heading('Boolean attributes', 'h3')}
35+
<p>
36+
By default, Markup handles all boolean attributes based on value.
37+
</p>
38+
${CodeSnippet(
39+
'const disabled = true;\n' +
40+
'\n' +
41+
'html`<button disabled="${disabled}">click me</button>`;',
42+
'typescript'
43+
)}
44+
<p><a
45+
href="https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML"
46+
>Boolean attributes</a
47+
> values in native HTML does not matter in whether the attribute
48+
should have an effect or be included. In Markup, if you set boolean attribute values
49+
to <code>FALSY</code> it will not be included.</p>
5650
<p>
57-
<a
58-
href="https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML"
59-
>Boolean attributes</a
60-
>
61-
are attributes that affect the element by
62-
simply being on the tag or whether they have value of
63-
<code>true</code> or <code>false</code>. HTML natively have these.
64-
</p>
65-
<p>
66-
The boolean attribute pattern is simple: <code>NAME="CONDITION"</code>. The <code>attr.</code> prefix is NOT
67-
required.
51+
The boolean attribute pattern is simply: <code>NAME="CONDITION"</code>.
6852
</p>
6953
${CodeSnippet(
7054
'html`<input type="checkbox" checked="true"/>`;',
7155
'typescript'
7256
)}
7357
<p>
7458
You may directly set the value as <code>true</code> or
75-
<code>false</code> string values or add a variable.
59+
<code>false</code> string or inject a variable.
7660
</p>
7761
${CodeSnippet(
7862
'const checked = false;\n\n' +
@@ -82,7 +66,7 @@ export default ({
8266
${Heading('The class attribute', 'h3')}
8367
<p>
8468
The class attribute has a special handle that allows you to
85-
dynamically set specific classes more elegantly.
69+
dynamically target single classes to be conditionally set.
8670
</p>
8771
<p>
8872
The class attribute pattern can be a key-value type
@@ -97,8 +81,7 @@ export default ({
9781
'// renders: <button class="primary btn">click me</button>\n',
9882
'typescript'
9983
)}
100-
<div class="info">You need to use the <code>|</code> (pipe symbol) to separate the value from the condition
101-
and the <code>attr.</code> prefix is not required.
84+
<div class="info">You need to use the <code>|</code> (pipe) to separate the value from the condition.
10285
</div>
10386
${Heading('The style attribute', 'h3')}
10487
<p>
@@ -118,7 +101,6 @@ export default ({
118101
'// renders: <button style="color: orange">click me</button>\n',
119102
'typescript'
120103
)}
121-
<p>The <code>attr.</code> prefix is not required for style attributes.</p>
122104
${Heading('The data attribute', 'h3')}
123105
<p>Data attributes follow the pattern: <code>data.NAME="VALUE | CONDITION"</code> and
124106
can also be <code>data.NAME="CONDITION"</code> if value is same as the condition value.
@@ -129,21 +111,15 @@ export default ({
129111
'html`<button data.loading="${loading}" data.btn="true">click me</button>`',
130112
'typescript'
131113
)}
132-
<p>The <code>attr.</code> prefix is not required for data attributes.</p>
133114
${Heading('Other attributes', 'h3')}
134-
<p>
135-
Everything else will fall into the category of a key-value pairs
136-
which is a collection of attributes that require specific values or
137-
work as "prop" for a tag to pass data or set configurations.
138-
</p>
139-
<p>All other attributes follow the pattern: <code>attr.NAME="VALUE | CONDITION"</code> or
140-
<code>attr.NAME="CONDITION"</code> if value is same as the condition value.
141-
The template will evaluate if the value is truthy or falsy to decide
115+
<p>For any other attribute you will need to either prefix the attribute with <code>attr.</code> or <code>|</code>(pipe) the value to a condition.</p>
116+
<p>These follow the pattern: <code>NAME="VALUE | CONDITION"</code> or <code>attr.NAME="VALUE_SAME_AS_CONDITION"</code>.
117+
The template will evaluate if the condition is truthy or falsy to decide
142118
whether the attribute should be set.</p>
143119
${CodeSnippet(
144120
'const label = "action button";\n\n' +
145121
'// will not render if label is an empty string\n' +
146-
'html`<button attr.aria-label="${label}">click me</button>`',
122+
'html`<button attr.aria-label="${label}" formenctype="multipart/form-data | ${isFormData}">click me</button>`',
147123
'typescript'
148124
)}
149125
`,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@beforesemicolon/markup",
3-
"version": "0.14.3",
3+
"version": "0.15.0",
44
"description": "Reactive HTML Templating System",
55
"engines": {
66
"node": ">=18.16.0"

src/executable/Doc.ts

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,40 @@ const node = (
3434
return
3535
}
3636

37-
if (value.trim()) {
37+
const trimmedValue = value.trim()
38+
39+
if (trimmedValue) {
40+
if (name === 'ref') {
41+
if (!refs[trimmedValue]) {
42+
refs[trimmedValue] = new Set()
43+
}
44+
45+
refs[trimmedValue].add(node as Element)
46+
return
47+
}
48+
49+
const attrLessName = name.replace(/^attr\./, '')
50+
const isBollAttr =
51+
booleanAttributes[
52+
attrLessName.toLowerCase() as keyof typeof booleanAttributes
53+
]
54+
55+
// boolean attr with false value can just be ignored
56+
if (trimmedValue === 'false' && isBollAttr) {
57+
return
58+
}
59+
3860
let e: ExecutableValue = {
3961
name,
40-
value,
41-
rawValue: value,
62+
value: trimmedValue,
63+
rawValue: trimmedValue,
4264
renderedNodes: [node],
43-
parts: extractExecutableValueFromRawValue(value, values),
65+
parts: /^(true|false)$/.test(trimmedValue)
66+
? [Boolean(trimmedValue)]
67+
: extractExecutableValueFromRawValue(
68+
trimmedValue,
69+
values
70+
),
4471
}
4572

4673
if (/^on[a-z]+/.test(name)) {
@@ -69,23 +96,10 @@ const node = (
6996
}
7097
}
7198

72-
if (name === 'ref') {
73-
if (!refs[value]) {
74-
refs[value] = new Set()
75-
}
76-
77-
refs[value].add(node as Element)
78-
return
79-
}
80-
81-
const attrLessName = name.replace(/^attr\./, '')
82-
8399
if (
84-
name.startsWith('attr.') ||
85-
booleanAttributes[
86-
attrLessName.toLowerCase() as keyof typeof booleanAttributes
87-
] ||
88-
/^(class|style|data)/i.test(attrLessName)
100+
isBollAttr ||
101+
/^(attr|class|style|data)/i.test(name) ||
102+
trimmedValue.split(/\|/).length > 1
89103
) {
90104
let props: string[] = []
91105
;[name, ...props] = attrLessName.split('.')
@@ -99,14 +113,20 @@ const node = (
99113

100114
handleAttrDirectiveExecutableValue(e)
101115
return cb(node, e, 'directives')
102-
} else if (/{{val[0-9]+}}/.test(value)) {
116+
}
117+
118+
if (/{{val[0-9]+}}/.test(trimmedValue)) {
103119
handleAttrExecutableValue(e, node as Element)
104120
return cb(node, e, 'attributes')
105121
}
106122
}
107123

108-
if ('setAttribute' in node) {
109-
node.setAttribute(name, value)
124+
if (
125+
'setAttribute' in node &&
126+
// ignore special attributes specific to Markup that did not get handled
127+
!/^(ref|(attr|class|style|data)\.)/.test(name)
128+
) {
129+
node.setAttribute(name, trimmedValue)
110130
}
111131
},
112132
appendChild: (n: DocumentFragment | Node) => {

src/html.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,14 @@ describe('html', () => {
495495
expect(divElement).toBeInstanceOf(HTMLDivElement)
496496
})
497497

498+
it('should handle empty ref directive', () => {
499+
const btn = html`<button ref="">click me</button>`
500+
501+
btn.render(document.body)
502+
503+
expect(document.body.innerHTML).toBe('<button>click me</button>');
504+
})
505+
498506
it('should handle ref directive on dynamic elements', () => {
499507
let x = 15
500508
const label = html`${when(
@@ -601,6 +609,14 @@ describe('html', () => {
601609
expect(document.body.innerHTML).toBe('<button>click me</button>')
602610
})
603611

612+
it('empty class should be ignored', () => {
613+
const btn = html`<button attr.class="" class.sample="">click me</button>`
614+
615+
btn.render(document.body)
616+
617+
expect(document.body.innerHTML).toBe('<button>click me</button>');
618+
})
619+
604620
it('data name as property', () => {
605621
let loading = true
606622
const btn = html`
@@ -701,6 +717,14 @@ describe('html', () => {
701717
expect(document.body.innerHTML).toBe('<button>click me</button>')
702718
})
703719

720+
it('empty data should be ignored', () => {
721+
const btn = html`<button attr.data="" data.sample="">click me</button>`
722+
723+
btn.render(document.body)
724+
725+
expect(document.body.innerHTML).toBe('<button>click me</button>');
726+
})
727+
704728
it('style property without flag', () => {
705729
const btn = html`
706730
<button attr.style.cursor="pointer">click me</button>`
@@ -817,6 +841,14 @@ describe('html', () => {
817841
)
818842
})
819843

844+
it('empty style should be ignored', () => {
845+
const btn = html`<button attr.style="" style.color="">click me</button>`
846+
847+
btn.render(document.body)
848+
849+
expect(document.body.innerHTML).toBe('<button>click me</button>');
850+
})
851+
820852
it('any boolean attr', () => {
821853
let disabled = true
822854
const btn = html`
@@ -958,6 +990,21 @@ describe('html', () => {
958990
expect(document.body.innerHTML).toBe('<input pattern="[a-z]">')
959991
})
960992

993+
it('any key-value pair without .attr', () => {
994+
let pattern = ''
995+
const field = html`<input pattern="${() => pattern} | ${() => pattern}"/>`
996+
997+
field.render(document.body)
998+
999+
expect(document.body.innerHTML).toBe('<input>')
1000+
1001+
pattern = '[a-z]'
1002+
1003+
field.update()
1004+
1005+
expect(document.body.innerHTML).toBe('<input pattern="[a-z]">')
1006+
})
1007+
9611008
it('should work with helper value', () => {
9621009
const is = helper(<T>(st: () => T, val: unknown) => st() === val);
9631010
const [disabled, setDisabled] = state(false);
@@ -991,6 +1038,14 @@ describe('html', () => {
9911038

9921039
expect(document.body.innerHTML).toBe('<slot></slot><slot name="123"></slot>')
9931040
});
1041+
1042+
it('should handle slot name without attr.', () => {
1043+
const slotName = '123'
1044+
1045+
html`<slot name="${slotName} | ${false}"></slot><slot name="${slotName} | ${true}"></slot>`.render(document.body)
1046+
1047+
expect(document.body.innerHTML).toBe('<slot></slot><slot name="123"></slot>')
1048+
});
9941049
})
9951050

9961051
it('should handle primitive attribute value', () => {

src/html.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,16 @@ export class HtmlTemplate {
199199
}
200200

201201
unmount() {
202-
this.#nodes.forEach((n) => {
203-
if (n.parentNode) {
204-
n.parentNode.removeChild(n)
205-
}
206-
})
207-
this.#renderTarget = null
208-
this.unsubscribeFromStates()
209-
this.#broadcast(this.#unmountSubs)
202+
if (this.#renderTarget) {
203+
this.#nodes.forEach((n) => {
204+
if (n.parentNode) {
205+
n.parentNode.removeChild(n)
206+
}
207+
})
208+
this.#renderTarget = null
209+
this.unsubscribeFromStates()
210+
this.#broadcast(this.#unmountSubs)
211+
}
210212
}
211213

212214
unsubscribeFromStates = () => {

0 commit comments

Comments
 (0)