Skip to content

Commit

Permalink
Release 1.14.1 (#10)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ECorreia45 authored Dec 25, 2023
1 parent 51935a4 commit 4a28a63
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 88 deletions.
10 changes: 10 additions & 0 deletions docs-src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import FunctionComponentsPage from './documentation/function-components.page'
import WebComponentsPage from './documentation/web-components.page'
import ServerSideRenderingPage from './documentation/server-side-rendering.page'
import StateStorePage from './documentation/state-store.page'
import TemplateLifecyclesPage from './documentation/template-lifecycles.page'
import { DocumentsGroup, Page } from './type'

const genericDescription =
Expand Down Expand Up @@ -150,6 +151,15 @@ const config: { name: string; pages: Page[] } = {
group: 'Templating',
root: false,
},
{
path: '/documentation/template-lifecycles',
name: 'Template Lifecycles',
title: 'Documentation: Template Lifecycles',
description: genericDescription,
component: TemplateLifecyclesPage,
group: 'Templating',
root: false,
},
{
path: '/documentation/what-is-a-helper',
name: 'What is a Helper?',
Expand Down
13 changes: 0 additions & 13 deletions docs-src/documentation/creating-and-rendering.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,5 @@ export default ({
render it in a different place, it will be automatically removed
from the previous place.
</p>
${Heading('Unmounting', 'h3')}
<p>
Another method you have available is the
<code>unmount</code> which gives you the ability to unmount your
template the right way.
</p>
${CodeSnippet('temp.unmount()', 'typescript')}
<p>
The unmount method will unsubscribe from any
<a href="./state-values">state</a> and reset the template
instance to its original state ready to be re-rendered by
calling the <code>render</code> method.
</p>
`,
})
142 changes: 142 additions & 0 deletions docs-src/documentation/template-lifecycles.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { html } from '../../src'
import { DocPageLayout } from '../partials/doc-page-layout'
import { Heading } from '../partials/heading'
import { PageComponentProps } from '../type'
import { CodeSnippet } from '../partials/code-snippet'

export default ({
name,
page,
nextPage,
prevPage,
docsMenu,
}: PageComponentProps) =>
DocPageLayout({
name,
page,
prevPage,
nextPage,
docsMenu,
content: html`
${Heading(page.name)}
<p>
Markup templates offer a convenient way to tap into its
lifecycles, so you can perform setup and teardown actions.
</p>
${Heading('onMount', 'h3')}
<p>
The <code>onMount</code> lifecycle allows you to react to when
the template is rendered. This is triggered whenever the
<code>render</code> and <code>replace</code>
methods successfully render the nodes in the provided target.
</p>
${CodeSnippet(
'const temp = html`\n' +
' <p>sample</p>\n' +
'`;\n' +
'\n' +
'temp.onMount(() => {\n' +
' // handle mount\n' +
'})\n' +
'\n' +
'temp.render(document.body)',
'typescript'
)}
<p>
You can always check if the place the template was rendered is
in the DOM by checking the <code>isConnected</code> on the
<code>renderTarget</code>.
</p>
${CodeSnippet('temp.renderTarget.isConnected;', 'typescript')}
${Heading('onUnmount', 'h3')}
<p>
The <code>onUnmount</code> lifecycle allows you to react to when
the template is removed from the element it was rendered. This
is triggered whenever the <code>unmount</code> method
successfully unmounts the template.
</p>
${CodeSnippet(
'const temp = html`\n' +
' <p>sample</p>\n' +
'`;\n' +
'\n' +
'temp.onUnmount(() => {\n' +
' // handle unmount\n' +
'})\n' +
'\n' +
'temp.render(document.body)',
'typescript'
)}
<p>
You can call the <code>unmount</code> method directly in the
code but Markup also tracks templates behind the scenes
individually.
</p>
<p>
Whenever templates are no longer needed, the
<code>unmount</code> method is called to remove them. Thus, all
the cleanup for the template is performed.
</p>
${Heading('onUpdate', 'h3')}
<p>
The <code>onUpdate</code> lifecycle allows you to react to when
an update is requested for the template. This can be by calling
the <code>update</code> method or automatically is you are using
<a href="./state-values">state</a>.
</p>
${CodeSnippet(
'const [count, setCount] = state(0);\n\n' +
'const temp = html`\n' +
' <p>${count}</p>\n' +
'`;\n' +
'\n' +
'temp.onUpdate(() => {\n' +
' // handle update\n' +
'})\n' +
'\n' +
'temp.render(document.body)',
'typescript'
)}
${Heading('Chainable methods', 'h3')}
<p>
Markup allows you to chain the following methods:
<code>render</code>, <code>replace</code>, <code>onMount</code>,
<code>onUnmount</code>, and <code>onUpdate</code>.
</p>
${CodeSnippet(
'html`<p>sample</p>`\n' +
' .onMount(() => {\n' +
' // handle mount\n' +
' })\n' +
' .onUnmount(() => {\n' +
' // handle unmount\n' +
' })\n' +
' .onUpdate(() => {\n' +
' // handle update\n' +
' })\n' +
' .render(document.body)',
'typescript'
)}
<p>
This makes it easy to handle things in a function where you need
to return the template.
</p>
${CodeSnippet(
'const Button = ({content, type, disabled}) => {\n' +
' \n' +
' return html`\n' +
' <button\n' +
' type="${type}"\n' +
' disabled="${disabled}"\n' +
' >\n' +
' ${content}\n' +
' </button>\n' +
' `\n' +
' .onUpdate(() => {\n' +
' // handle update\n' +
' })\n' +
'}',
'typescript'
)}
`,
})
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.13.3",
"version": "0.14.1",
"description": "Reactive HTML Templating System",
"engines": {
"node": ">=18.16.0"
Expand Down
28 changes: 19 additions & 9 deletions src/executable/handle-executable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,12 @@ export function handleTextExecutableValue(
refs: Record<string, Set<Element>>,
el: Node
) {
const value = partsToValue(val.parts)
const value = partsToValue(val.parts) as Array<Node | HtmlTemplate | string>
const nodes: Array<Node> = []

let idx = 0
for (const v of value as Array<Node | HtmlTemplate | string>) {
for (let i = 0; i < value.length; i++) {
const v = value[i]

if (v instanceof HtmlTemplate) {
const renderedBefore = v.renderTarget !== null

Expand Down Expand Up @@ -182,20 +183,29 @@ export function handleTextExecutableValue(
// to avoid unnecessary DOM updates
if (
Array.isArray(val.value) &&
String(val.value[idx]) === String(v)
String(val.value[i]) === String(v)
) {
nodes.push(val.renderedNodes[idx])
nodes.push(val.renderedNodes[i])
} else {
nodes.push(document.createTextNode(String(v)))
}
}

idx += 1
}

val.value = value

// need to make sure nodes array does not have repeated nodes
// which cannot be rendered in 2 places at once
handleTextExecutable(val, Array.from(new Set(nodes)), el)

// clean up templates removed by unmounting them
if (Array.isArray(val.value)) {
const valueSet = new Set(value)

for (const v of val.value as unknown[]) {
if (v instanceof HtmlTemplate && !valueSet.has(v)) {
v.unmount()
}
}
}

val.value = value
}
107 changes: 86 additions & 21 deletions src/html.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {html, HtmlTemplate, state} from './html'
import {when, repeat} from './helpers'
import {when, repeat, oneOf} from './helpers'
import {suspense} from './utils'
import {helper} from "./Helper";

Expand Down Expand Up @@ -1269,15 +1269,15 @@ describe('html', () => {
<div class="todo-actions">
${when(
() => this.#status === 'pending',
html`${completeBtn}${editBtn}${archiveBtn}`
html`${completeBtn}${editBtn}`
)}
${when(
() => this.#status === 'archived',
html`${progressBtn}${deleteBtn}`
oneOf(this.#status, ['completed', 'pending']),
archiveBtn
)}
${when(
() => this.#status === 'completed',
archiveBtn
() => this.#status === 'archived',
html`${progressBtn}${deleteBtn}`
)}
</div>
</div>`
Expand Down Expand Up @@ -1336,8 +1336,8 @@ describe('html', () => {
expect(todo.shadowRoot?.innerHTML).toBe('<div class="todo-item">\n' +
'\t\t\t\t\t\t\t<div class="details">\n' +
'\t\t\t\t\t\t\t\t<h3>sample</h3></div>\n' +
'\t\t\t\t\t\t\t<div class="todo-actions"><button>complete</button><button>edit</button><button>archive</button>\n' +
'\t\t\t\t\t\t\t\t\n' +
'\t\t\t\t\t\t\t<div class="todo-actions"><button>complete</button><button>edit</button>\n' +
'\t\t\t\t\t\t\t\t<button>archive</button>\n' +
'\t\t\t\t\t\t\t\t</div>\n' +
'\t\t\t\t\t\t</div>')

Expand All @@ -1348,11 +1348,10 @@ describe('html', () => {
expect(document.body.innerHTML).toBe(
'<todo-item name="sample" description="" status="completed"></todo-item>'
)
expect(todo.shadowRoot?.innerHTML).toBe(
'<div class="todo-item">\n' +
expect(todo.shadowRoot?.innerHTML).toBe('<div class="todo-item">\n' +
'\t\t\t\t\t\t\t<div class="details">\n' +
'\t\t\t\t\t\t\t\t<h3>sample</h3></div>\n' +
'\t\t\t\t\t\t\t<div class="todo-actions"><button>edit</button><button>archive</button>\n' +
'\t\t\t\t\t\t\t<div class="todo-actions"><button>archive</button>\n' +
'\t\t\t\t\t\t\t\t</div>\n' +
'\t\t\t\t\t\t</div>'
)
Expand Down Expand Up @@ -1636,21 +1635,87 @@ describe('html', () => {
expect(document.body.innerHTML).toBe('<span>1</span><button>+</button>')
});

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

const counter = html`<span>${count}</span>`
counter.onUpdate(updateMock)
counter.render(document.body)
it('onUpdate', () => {
const [count, setCount] = state<number>(0)
const updateMock = jest.fn()

const counter = html`<span>${count}</span>`
counter.onUpdate(updateMock)
counter.render(document.body)

expect(document.body.innerHTML).toBe('<span>0</span>')

setCount((prev) => prev + 1)

jest.advanceTimersByTime(100);

expect(updateMock).toHaveBeenCalledTimes(1)

expect(document.body.innerHTML).toBe('<span>1</span>')
})

expect(document.body.innerHTML).toBe('<span>0</span>')
it('onMount', () => {

setCount((prev) => prev + 1)
const mountMock = jest.fn()

html`<span>sample</span>`
.onMount(mountMock)
.render(document.body)

jest.advanceTimersByTime(100);

expect(mountMock).toHaveBeenCalledTimes(1)
});

expect(updateMock).toHaveBeenCalledTimes(1)
it('onUnmount', () => {
const unmountMock = jest.fn()

const temp = html`<span>sample</span>`
.onUnmount(unmountMock)
.render(document.body)

temp.unmount();

jest.advanceTimersByTime(100);

expect(unmountMock).toHaveBeenCalledTimes(1)
});

expect(document.body.innerHTML).toBe('<span>1</span>')
it('onUnmount on removed item', () => {
const unmountMock = jest.fn();
const list = [
html`one`.onUnmount(unmountMock),
html`two`.onUnmount(unmountMock),
html`three`.onUnmount(unmountMock),
]

const temp = html`${() => list}`
.render(document.body)

expect(document.body.innerHTML).toBe('onetwothree')

list.splice(1, 1);
const three = list.splice(1, 1);

temp.update();

expect(document.body.innerHTML).toBe('one')

jest.advanceTimersByTime(100);

expect(unmountMock).toHaveBeenCalledTimes(2)

list.unshift(...three);

temp.update();

expect(document.body.innerHTML).toBe('threeone')
});
})

it('should ignore values between tag and attribute', () => {
Expand Down
Loading

0 comments on commit 4a28a63

Please sign in to comment.