Skip to content

Commit 4a28a63

Browse files
authored
Release 1.14.1 (#10)
* introduce onMount and onUnmount (#9) * introduce onMount and onUnmount * template lifecycle doc * bump next version * do not unmount values templates * minor version bump * make lifecycles async with settimeout * adjust version to 0.14.0
1 parent 51935a4 commit 4a28a63

File tree

7 files changed

+342
-88
lines changed

7 files changed

+342
-88
lines changed

docs-src/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import FunctionComponentsPage from './documentation/function-components.page'
2525
import WebComponentsPage from './documentation/web-components.page'
2626
import ServerSideRenderingPage from './documentation/server-side-rendering.page'
2727
import StateStorePage from './documentation/state-store.page'
28+
import TemplateLifecyclesPage from './documentation/template-lifecycles.page'
2829
import { DocumentsGroup, Page } from './type'
2930

3031
const genericDescription =
@@ -150,6 +151,15 @@ const config: { name: string; pages: Page[] } = {
150151
group: 'Templating',
151152
root: false,
152153
},
154+
{
155+
path: '/documentation/template-lifecycles',
156+
name: 'Template Lifecycles',
157+
title: 'Documentation: Template Lifecycles',
158+
description: genericDescription,
159+
component: TemplateLifecyclesPage,
160+
group: 'Templating',
161+
root: false,
162+
},
153163
{
154164
path: '/documentation/what-is-a-helper',
155165
name: 'What is a Helper?',

docs-src/documentation/creating-and-rendering.page.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,5 @@ export default ({
108108
render it in a different place, it will be automatically removed
109109
from the previous place.
110110
</p>
111-
${Heading('Unmounting', 'h3')}
112-
<p>
113-
Another method you have available is the
114-
<code>unmount</code> which gives you the ability to unmount your
115-
template the right way.
116-
</p>
117-
${CodeSnippet('temp.unmount()', 'typescript')}
118-
<p>
119-
The unmount method will unsubscribe from any
120-
<a href="./state-values">state</a> and reset the template
121-
instance to its original state ready to be re-rendered by
122-
calling the <code>render</code> method.
123-
</p>
124111
`,
125112
})
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { html } from '../../src'
2+
import { DocPageLayout } from '../partials/doc-page-layout'
3+
import { Heading } from '../partials/heading'
4+
import { PageComponentProps } from '../type'
5+
import { CodeSnippet } from '../partials/code-snippet'
6+
7+
export default ({
8+
name,
9+
page,
10+
nextPage,
11+
prevPage,
12+
docsMenu,
13+
}: PageComponentProps) =>
14+
DocPageLayout({
15+
name,
16+
page,
17+
prevPage,
18+
nextPage,
19+
docsMenu,
20+
content: html`
21+
${Heading(page.name)}
22+
<p>
23+
Markup templates offer a convenient way to tap into its
24+
lifecycles, so you can perform setup and teardown actions.
25+
</p>
26+
${Heading('onMount', 'h3')}
27+
<p>
28+
The <code>onMount</code> lifecycle allows you to react to when
29+
the template is rendered. This is triggered whenever the
30+
<code>render</code> and <code>replace</code>
31+
methods successfully render the nodes in the provided target.
32+
</p>
33+
${CodeSnippet(
34+
'const temp = html`\n' +
35+
' <p>sample</p>\n' +
36+
'`;\n' +
37+
'\n' +
38+
'temp.onMount(() => {\n' +
39+
' // handle mount\n' +
40+
'})\n' +
41+
'\n' +
42+
'temp.render(document.body)',
43+
'typescript'
44+
)}
45+
<p>
46+
You can always check if the place the template was rendered is
47+
in the DOM by checking the <code>isConnected</code> on the
48+
<code>renderTarget</code>.
49+
</p>
50+
${CodeSnippet('temp.renderTarget.isConnected;', 'typescript')}
51+
${Heading('onUnmount', 'h3')}
52+
<p>
53+
The <code>onUnmount</code> lifecycle allows you to react to when
54+
the template is removed from the element it was rendered. This
55+
is triggered whenever the <code>unmount</code> method
56+
successfully unmounts the template.
57+
</p>
58+
${CodeSnippet(
59+
'const temp = html`\n' +
60+
' <p>sample</p>\n' +
61+
'`;\n' +
62+
'\n' +
63+
'temp.onUnmount(() => {\n' +
64+
' // handle unmount\n' +
65+
'})\n' +
66+
'\n' +
67+
'temp.render(document.body)',
68+
'typescript'
69+
)}
70+
<p>
71+
You can call the <code>unmount</code> method directly in the
72+
code but Markup also tracks templates behind the scenes
73+
individually.
74+
</p>
75+
<p>
76+
Whenever templates are no longer needed, the
77+
<code>unmount</code> method is called to remove them. Thus, all
78+
the cleanup for the template is performed.
79+
</p>
80+
${Heading('onUpdate', 'h3')}
81+
<p>
82+
The <code>onUpdate</code> lifecycle allows you to react to when
83+
an update is requested for the template. This can be by calling
84+
the <code>update</code> method or automatically is you are using
85+
<a href="./state-values">state</a>.
86+
</p>
87+
${CodeSnippet(
88+
'const [count, setCount] = state(0);\n\n' +
89+
'const temp = html`\n' +
90+
' <p>${count}</p>\n' +
91+
'`;\n' +
92+
'\n' +
93+
'temp.onUpdate(() => {\n' +
94+
' // handle update\n' +
95+
'})\n' +
96+
'\n' +
97+
'temp.render(document.body)',
98+
'typescript'
99+
)}
100+
${Heading('Chainable methods', 'h3')}
101+
<p>
102+
Markup allows you to chain the following methods:
103+
<code>render</code>, <code>replace</code>, <code>onMount</code>,
104+
<code>onUnmount</code>, and <code>onUpdate</code>.
105+
</p>
106+
${CodeSnippet(
107+
'html`<p>sample</p>`\n' +
108+
' .onMount(() => {\n' +
109+
' // handle mount\n' +
110+
' })\n' +
111+
' .onUnmount(() => {\n' +
112+
' // handle unmount\n' +
113+
' })\n' +
114+
' .onUpdate(() => {\n' +
115+
' // handle update\n' +
116+
' })\n' +
117+
' .render(document.body)',
118+
'typescript'
119+
)}
120+
<p>
121+
This makes it easy to handle things in a function where you need
122+
to return the template.
123+
</p>
124+
${CodeSnippet(
125+
'const Button = ({content, type, disabled}) => {\n' +
126+
' \n' +
127+
' return html`\n' +
128+
' <button\n' +
129+
' type="${type}"\n' +
130+
' disabled="${disabled}"\n' +
131+
' >\n' +
132+
' ${content}\n' +
133+
' </button>\n' +
134+
' `\n' +
135+
' .onUpdate(() => {\n' +
136+
' // handle update\n' +
137+
' })\n' +
138+
'}',
139+
'typescript'
140+
)}
141+
`,
142+
})

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.13.3",
3+
"version": "0.14.1",
44
"description": "Reactive HTML Templating System",
55
"engines": {
66
"node": ">=18.16.0"

src/executable/handle-executable.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,12 @@ export function handleTextExecutableValue(
146146
refs: Record<string, Set<Element>>,
147147
el: Node
148148
) {
149-
const value = partsToValue(val.parts)
149+
const value = partsToValue(val.parts) as Array<Node | HtmlTemplate | string>
150150
const nodes: Array<Node> = []
151151

152-
let idx = 0
153-
for (const v of value as Array<Node | HtmlTemplate | string>) {
152+
for (let i = 0; i < value.length; i++) {
153+
const v = value[i]
154+
154155
if (v instanceof HtmlTemplate) {
155156
const renderedBefore = v.renderTarget !== null
156157

@@ -182,20 +183,29 @@ export function handleTextExecutableValue(
182183
// to avoid unnecessary DOM updates
183184
if (
184185
Array.isArray(val.value) &&
185-
String(val.value[idx]) === String(v)
186+
String(val.value[i]) === String(v)
186187
) {
187-
nodes.push(val.renderedNodes[idx])
188+
nodes.push(val.renderedNodes[i])
188189
} else {
189190
nodes.push(document.createTextNode(String(v)))
190191
}
191192
}
192-
193-
idx += 1
194193
}
195194

196-
val.value = value
197-
198195
// need to make sure nodes array does not have repeated nodes
199196
// which cannot be rendered in 2 places at once
200197
handleTextExecutable(val, Array.from(new Set(nodes)), el)
198+
199+
// clean up templates removed by unmounting them
200+
if (Array.isArray(val.value)) {
201+
const valueSet = new Set(value)
202+
203+
for (const v of val.value as unknown[]) {
204+
if (v instanceof HtmlTemplate && !valueSet.has(v)) {
205+
v.unmount()
206+
}
207+
}
208+
}
209+
210+
val.value = value
201211
}

src/html.spec.ts

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {html, HtmlTemplate, state} from './html'
2-
import {when, repeat} from './helpers'
2+
import {when, repeat, oneOf} from './helpers'
33
import {suspense} from './utils'
44
import {helper} from "./Helper";
55

@@ -1269,15 +1269,15 @@ describe('html', () => {
12691269
<div class="todo-actions">
12701270
${when(
12711271
() => this.#status === 'pending',
1272-
html`${completeBtn}${editBtn}${archiveBtn}`
1272+
html`${completeBtn}${editBtn}`
12731273
)}
12741274
${when(
1275-
() => this.#status === 'archived',
1276-
html`${progressBtn}${deleteBtn}`
1275+
oneOf(this.#status, ['completed', 'pending']),
1276+
archiveBtn
12771277
)}
12781278
${when(
1279-
() => this.#status === 'completed',
1280-
archiveBtn
1279+
() => this.#status === 'archived',
1280+
html`${progressBtn}${deleteBtn}`
12811281
)}
12821282
</div>
12831283
</div>`
@@ -1336,8 +1336,8 @@ describe('html', () => {
13361336
expect(todo.shadowRoot?.innerHTML).toBe('<div class="todo-item">\n' +
13371337
'\t\t\t\t\t\t\t<div class="details">\n' +
13381338
'\t\t\t\t\t\t\t\t<h3>sample</h3></div>\n' +
1339-
'\t\t\t\t\t\t\t<div class="todo-actions"><button>complete</button><button>edit</button><button>archive</button>\n' +
1340-
'\t\t\t\t\t\t\t\t\n' +
1339+
'\t\t\t\t\t\t\t<div class="todo-actions"><button>complete</button><button>edit</button>\n' +
1340+
'\t\t\t\t\t\t\t\t<button>archive</button>\n' +
13411341
'\t\t\t\t\t\t\t\t</div>\n' +
13421342
'\t\t\t\t\t\t</div>')
13431343

@@ -1348,11 +1348,10 @@ describe('html', () => {
13481348
expect(document.body.innerHTML).toBe(
13491349
'<todo-item name="sample" description="" status="completed"></todo-item>'
13501350
)
1351-
expect(todo.shadowRoot?.innerHTML).toBe(
1352-
'<div class="todo-item">\n' +
1351+
expect(todo.shadowRoot?.innerHTML).toBe('<div class="todo-item">\n' +
13531352
'\t\t\t\t\t\t\t<div class="details">\n' +
13541353
'\t\t\t\t\t\t\t\t<h3>sample</h3></div>\n' +
1355-
'\t\t\t\t\t\t\t<div class="todo-actions"><button>edit</button><button>archive</button>\n' +
1354+
'\t\t\t\t\t\t\t<div class="todo-actions"><button>archive</button>\n' +
13561355
'\t\t\t\t\t\t\t\t</div>\n' +
13571356
'\t\t\t\t\t\t</div>'
13581357
)
@@ -1636,21 +1635,87 @@ describe('html', () => {
16361635
expect(document.body.innerHTML).toBe('<span>1</span><button>+</button>')
16371636
});
16381637

1639-
it('should handle onUpdate callback', () => {
1640-
const [count, setCount] = state<number>(0)
1641-
const updateMock = jest.fn()
1638+
describe('should handle lifecycles', () => {
1639+
beforeEach(() => {
1640+
jest.useFakeTimers()
1641+
})
16421642

1643-
const counter = html`<span>${count}</span>`
1644-
counter.onUpdate(updateMock)
1645-
counter.render(document.body)
1643+
it('onUpdate', () => {
1644+
const [count, setCount] = state<number>(0)
1645+
const updateMock = jest.fn()
1646+
1647+
const counter = html`<span>${count}</span>`
1648+
counter.onUpdate(updateMock)
1649+
counter.render(document.body)
1650+
1651+
expect(document.body.innerHTML).toBe('<span>0</span>')
1652+
1653+
setCount((prev) => prev + 1)
1654+
1655+
jest.advanceTimersByTime(100);
1656+
1657+
expect(updateMock).toHaveBeenCalledTimes(1)
1658+
1659+
expect(document.body.innerHTML).toBe('<span>1</span>')
1660+
})
16461661

1647-
expect(document.body.innerHTML).toBe('<span>0</span>')
1662+
it('onMount', () => {
16481663

1649-
setCount((prev) => prev + 1)
1664+
const mountMock = jest.fn()
1665+
1666+
html`<span>sample</span>`
1667+
.onMount(mountMock)
1668+
.render(document.body)
1669+
1670+
jest.advanceTimersByTime(100);
1671+
1672+
expect(mountMock).toHaveBeenCalledTimes(1)
1673+
});
16501674

1651-
expect(updateMock).toHaveBeenCalledTimes(1)
1675+
it('onUnmount', () => {
1676+
const unmountMock = jest.fn()
1677+
1678+
const temp = html`<span>sample</span>`
1679+
.onUnmount(unmountMock)
1680+
.render(document.body)
1681+
1682+
temp.unmount();
1683+
1684+
jest.advanceTimersByTime(100);
1685+
1686+
expect(unmountMock).toHaveBeenCalledTimes(1)
1687+
});
16521688

1653-
expect(document.body.innerHTML).toBe('<span>1</span>')
1689+
it('onUnmount on removed item', () => {
1690+
const unmountMock = jest.fn();
1691+
const list = [
1692+
html`one`.onUnmount(unmountMock),
1693+
html`two`.onUnmount(unmountMock),
1694+
html`three`.onUnmount(unmountMock),
1695+
]
1696+
1697+
const temp = html`${() => list}`
1698+
.render(document.body)
1699+
1700+
expect(document.body.innerHTML).toBe('onetwothree')
1701+
1702+
list.splice(1, 1);
1703+
const three = list.splice(1, 1);
1704+
1705+
temp.update();
1706+
1707+
expect(document.body.innerHTML).toBe('one')
1708+
1709+
jest.advanceTimersByTime(100);
1710+
1711+
expect(unmountMock).toHaveBeenCalledTimes(2)
1712+
1713+
list.unshift(...three);
1714+
1715+
temp.update();
1716+
1717+
expect(document.body.innerHTML).toBe('threeone')
1718+
});
16541719
})
16551720

16561721
it('should ignore values between tag and attribute', () => {

0 commit comments

Comments
 (0)