Skip to content

Commit

Permalink
Next 0.15.0 (#11)
Browse files Browse the repository at this point in the history
* further remove the need to include the attr. for conditional arttributes

* only unmount if rendered target

* simplify atrribute handling
  • Loading branch information
ECorreia45 authored Dec 31, 2023
1 parent 817a7a6 commit c16b038
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 81 deletions.
74 changes: 25 additions & 49 deletions docs-src/documentation/conditional-attributes.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default ({
content: html`
${Heading(page.name)}
<p>
You cannot use template literal value to define attributes directly on the
Markup does not allow you to use template literal value to define attributes directly on the
tag.
</p>
${CodeSnippet(
Expand All @@ -30,49 +30,33 @@ export default ({
'// renders <button>click me</button>',
'typescript'
)}
<p>
This means that you need another way to dynamically render
attributes and that way is the Markup <code>attr</code> attribute's name prefixer.
</p>
${CodeSnippet(
'const disabled = true;\n' +
'\n' +
'html`<button attr.disabled="${disabled}">click me</button>`;',
'typescript'
)}
<p>
In the above example the <code>disabled</code> attribute
was prefixed with <code>attr.</code> then provided the condition(boolean) as
value to whether include that attribute.
</p>
<div class="info">
The <code>attr.</code> is not always needed. Attributes like <code>class</code>, <code>style</code>, <code>data</code>,
and <a href="https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML" target="_blank">HTML boolean
attributes</a>
work without it or can have the condition specified after the pipe <code>|</code> as the value. Everything
else requires it.
</div>
<p>There is a different way to go about conditionally set attributes.</p>
${Heading('Boolean attributes', 'h3')}
<p>
By default, Markup handles all boolean attributes based on value.
</p>
${CodeSnippet(
'const disabled = true;\n' +
'\n' +
'html`<button disabled="${disabled}">click me</button>`;',
'typescript'
)}
<p><a
href="https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML"
>Boolean attributes</a
> values in native HTML does not matter in whether the attribute
should have an effect or be included. In Markup, if you set boolean attribute values
to <code>FALSY</code> it will not be included.</p>
<p>
<a
href="https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML"
>Boolean attributes</a
>
are attributes that affect the element by
simply being on the tag or whether they have value of
<code>true</code> or <code>false</code>. HTML natively have these.
</p>
<p>
The boolean attribute pattern is simple: <code>NAME="CONDITION"</code>. The <code>attr.</code> prefix is NOT
required.
The boolean attribute pattern is simply: <code>NAME="CONDITION"</code>.
</p>
${CodeSnippet(
'html`<input type="checkbox" checked="true"/>`;',
'typescript'
)}
<p>
You may directly set the value as <code>true</code> or
<code>false</code> string values or add a variable.
<code>false</code> string or inject a variable.
</p>
${CodeSnippet(
'const checked = false;\n\n' +
Expand All @@ -82,7 +66,7 @@ export default ({
${Heading('The class attribute', 'h3')}
<p>
The class attribute has a special handle that allows you to
dynamically set specific classes more elegantly.
dynamically target single classes to be conditionally set.
</p>
<p>
The class attribute pattern can be a key-value type
Expand All @@ -97,8 +81,7 @@ export default ({
'// renders: <button class="primary btn">click me</button>\n',
'typescript'
)}
<div class="info">You need to use the <code>|</code> (pipe symbol) to separate the value from the condition
and the <code>attr.</code> prefix is not required.
<div class="info">You need to use the <code>|</code> (pipe) to separate the value from the condition.
</div>
${Heading('The style attribute', 'h3')}
<p>
Expand All @@ -118,7 +101,6 @@ export default ({
'// renders: <button style="color: orange">click me</button>\n',
'typescript'
)}
<p>The <code>attr.</code> prefix is not required for style attributes.</p>
${Heading('The data attribute', 'h3')}
<p>Data attributes follow the pattern: <code>data.NAME="VALUE | CONDITION"</code> and
can also be <code>data.NAME="CONDITION"</code> if value is same as the condition value.
Expand All @@ -129,21 +111,15 @@ export default ({
'html`<button data.loading="${loading}" data.btn="true">click me</button>`',
'typescript'
)}
<p>The <code>attr.</code> prefix is not required for data attributes.</p>
${Heading('Other attributes', 'h3')}
<p>
Everything else will fall into the category of a key-value pairs
which is a collection of attributes that require specific values or
work as "prop" for a tag to pass data or set configurations.
</p>
<p>All other attributes follow the pattern: <code>attr.NAME="VALUE | CONDITION"</code> or
<code>attr.NAME="CONDITION"</code> if value is same as the condition value.
The template will evaluate if the value is truthy or falsy to decide
<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>
<p>These follow the pattern: <code>NAME="VALUE | CONDITION"</code> or <code>attr.NAME="VALUE_SAME_AS_CONDITION"</code>.
The template will evaluate if the condition is truthy or falsy to decide
whether the attribute should be set.</p>
${CodeSnippet(
'const label = "action button";\n\n' +
'// will not render if label is an empty string\n' +
'html`<button attr.aria-label="${label}">click me</button>`',
'html`<button attr.aria-label="${label}" formenctype="multipart/form-data | ${isFormData}">click me</button>`',
'typescript'
)}
`,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@beforesemicolon/markup",
"version": "0.14.3",
"version": "0.15.0",
"description": "Reactive HTML Templating System",
"engines": {
"node": ">=18.16.0"
Expand Down
66 changes: 43 additions & 23 deletions src/executable/Doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,40 @@ const node = (
return
}

if (value.trim()) {
const trimmedValue = value.trim()

if (trimmedValue) {
if (name === 'ref') {
if (!refs[trimmedValue]) {
refs[trimmedValue] = new Set()
}

refs[trimmedValue].add(node as Element)
return
}

const attrLessName = name.replace(/^attr\./, '')
const isBollAttr =
booleanAttributes[
attrLessName.toLowerCase() as keyof typeof booleanAttributes
]

// boolean attr with false value can just be ignored
if (trimmedValue === 'false' && isBollAttr) {
return
}

let e: ExecutableValue = {
name,
value,
rawValue: value,
value: trimmedValue,
rawValue: trimmedValue,
renderedNodes: [node],
parts: extractExecutableValueFromRawValue(value, values),
parts: /^(true|false)$/.test(trimmedValue)
? [Boolean(trimmedValue)]
: extractExecutableValueFromRawValue(
trimmedValue,
values
),
}

if (/^on[a-z]+/.test(name)) {
Expand Down Expand Up @@ -69,23 +96,10 @@ const node = (
}
}

if (name === 'ref') {
if (!refs[value]) {
refs[value] = new Set()
}

refs[value].add(node as Element)
return
}

const attrLessName = name.replace(/^attr\./, '')

if (
name.startsWith('attr.') ||
booleanAttributes[
attrLessName.toLowerCase() as keyof typeof booleanAttributes
] ||
/^(class|style|data)/i.test(attrLessName)
isBollAttr ||
/^(attr|class|style|data)/i.test(name) ||
trimmedValue.split(/\|/).length > 1
) {
let props: string[] = []
;[name, ...props] = attrLessName.split('.')
Expand All @@ -99,14 +113,20 @@ const node = (

handleAttrDirectiveExecutableValue(e)
return cb(node, e, 'directives')
} else if (/{{val[0-9]+}}/.test(value)) {
}

if (/{{val[0-9]+}}/.test(trimmedValue)) {
handleAttrExecutableValue(e, node as Element)
return cb(node, e, 'attributes')
}
}

if ('setAttribute' in node) {
node.setAttribute(name, value)
if (
'setAttribute' in node &&
// ignore special attributes specific to Markup that did not get handled
!/^(ref|(attr|class|style|data)\.)/.test(name)
) {
node.setAttribute(name, trimmedValue)
}
},
appendChild: (n: DocumentFragment | Node) => {
Expand Down
55 changes: 55 additions & 0 deletions src/html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,14 @@ describe('html', () => {
expect(divElement).toBeInstanceOf(HTMLDivElement)
})

it('should handle empty ref directive', () => {
const btn = html`<button ref="">click me</button>`

btn.render(document.body)

expect(document.body.innerHTML).toBe('<button>click me</button>');
})

it('should handle ref directive on dynamic elements', () => {
let x = 15
const label = html`${when(
Expand Down Expand Up @@ -601,6 +609,14 @@ describe('html', () => {
expect(document.body.innerHTML).toBe('<button>click me</button>')
})

it('empty class should be ignored', () => {
const btn = html`<button attr.class="" class.sample="">click me</button>`

btn.render(document.body)

expect(document.body.innerHTML).toBe('<button>click me</button>');
})

it('data name as property', () => {
let loading = true
const btn = html`
Expand Down Expand Up @@ -701,6 +717,14 @@ describe('html', () => {
expect(document.body.innerHTML).toBe('<button>click me</button>')
})

it('empty data should be ignored', () => {
const btn = html`<button attr.data="" data.sample="">click me</button>`

btn.render(document.body)

expect(document.body.innerHTML).toBe('<button>click me</button>');
})

it('style property without flag', () => {
const btn = html`
<button attr.style.cursor="pointer">click me</button>`
Expand Down Expand Up @@ -817,6 +841,14 @@ describe('html', () => {
)
})

it('empty style should be ignored', () => {
const btn = html`<button attr.style="" style.color="">click me</button>`

btn.render(document.body)

expect(document.body.innerHTML).toBe('<button>click me</button>');
})

it('any boolean attr', () => {
let disabled = true
const btn = html`
Expand Down Expand Up @@ -958,6 +990,21 @@ describe('html', () => {
expect(document.body.innerHTML).toBe('<input pattern="[a-z]">')
})

it('any key-value pair without .attr', () => {
let pattern = ''
const field = html`<input pattern="${() => pattern} | ${() => pattern}"/>`

field.render(document.body)

expect(document.body.innerHTML).toBe('<input>')

pattern = '[a-z]'

field.update()

expect(document.body.innerHTML).toBe('<input pattern="[a-z]">')
})

it('should work with helper value', () => {
const is = helper(<T>(st: () => T, val: unknown) => st() === val);
const [disabled, setDisabled] = state(false);
Expand Down Expand Up @@ -991,6 +1038,14 @@ describe('html', () => {

expect(document.body.innerHTML).toBe('<slot></slot><slot name="123"></slot>')
});

it('should handle slot name without attr.', () => {
const slotName = '123'

html`<slot name="${slotName} | ${false}"></slot><slot name="${slotName} | ${true}"></slot>`.render(document.body)

expect(document.body.innerHTML).toBe('<slot></slot><slot name="123"></slot>')
});
})

it('should handle primitive attribute value', () => {
Expand Down
18 changes: 10 additions & 8 deletions src/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,16 @@ export class HtmlTemplate {
}

unmount() {
this.#nodes.forEach((n) => {
if (n.parentNode) {
n.parentNode.removeChild(n)
}
})
this.#renderTarget = null
this.unsubscribeFromStates()
this.#broadcast(this.#unmountSubs)
if (this.#renderTarget) {
this.#nodes.forEach((n) => {
if (n.parentNode) {
n.parentNode.removeChild(n)
}
})
this.#renderTarget = null
this.unsubscribeFromStates()
this.#broadcast(this.#unmountSubs)
}
}

unsubscribeFromStates = () => {
Expand Down

0 comments on commit c16b038

Please sign in to comment.