v0.0.13: Refactored the render-loop to solve hard-to-track bugs and added more (elaborate) unit-tests to make sure it all works
This commit is contained in:
parent
b95e5506d2
commit
0da07549e7
@ -1 +1,3 @@
|
||||
TODO the root build scripts are yet to be updated (building/watching and serving examples...)
|
||||
TODO:
|
||||
- the root build scripts are yet to be updated (building/watching and serving examples...)
|
||||
- Key-property is currently intepreted as per render function, it is preferrably per level in the hierarchy
|
||||
@ -27,6 +27,9 @@
|
||||
<li>
|
||||
<a href="./table/">Tables (arrow functions)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./table-2/">Tables 2 (Key-prop)</a>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
10
examples/table-2/index.html
Normal file
10
examples/table-2/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Cerxes - CustomElements - SVG</title>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
132
examples/table-2/index.jsx
Normal file
132
examples/table-2/index.jsx
Normal file
@ -0,0 +1,132 @@
|
||||
import {render, CustomElement, defineElement, Host, prop, state} from "../../packages/csx";
|
||||
import style from "./index.scss";
|
||||
import {TableComponent} from "./table-component";
|
||||
|
||||
@defineElement("table-tester")
|
||||
class TableTester extends CustomElement{
|
||||
/**
|
||||
*
|
||||
* @type {({[headerRender]: (function(): string), render: (function(User): *), [size]: number})[]}
|
||||
*/
|
||||
#columnDefinitions = [
|
||||
{
|
||||
headerRender: () => "Id",
|
||||
render: (u) => u.userId,
|
||||
size: 110
|
||||
},
|
||||
{
|
||||
headerRender: () => "Email",
|
||||
render: (u) => u.identity?.email,
|
||||
},
|
||||
{
|
||||
headerRender: () => "FirstName",
|
||||
render: (u) => u.identity?.firstName,
|
||||
size: 160
|
||||
},
|
||||
{
|
||||
headerRender: () => "LastName",
|
||||
render: (u) => u.identity?.lastName,
|
||||
size: 160
|
||||
},
|
||||
{
|
||||
headerRender: () => "Up",
|
||||
render: (u) => {
|
||||
let d = new Date();
|
||||
return (<button onClick={(ev) => this.moveUp(ev, u, d)}>Up</button>)
|
||||
},
|
||||
size: 110
|
||||
},
|
||||
{
|
||||
headerRender: () => "Down",
|
||||
render: (u) => {
|
||||
let d = new Date();
|
||||
return (<button onClick={(ev) => this.moveDown(ev, u, d)}>Down</button>)
|
||||
},
|
||||
size: 110
|
||||
},
|
||||
];
|
||||
|
||||
@state()
|
||||
users = [];
|
||||
|
||||
rowKey = (user)=>user.userId;
|
||||
cellRef = (user, colIdx, el)=>user.cells[colIdx]=el;
|
||||
|
||||
render(){
|
||||
|
||||
return <TableComponent
|
||||
columns={this.#columnDefinitions}
|
||||
data={this.users}
|
||||
rowKey={this.rowKey}
|
||||
cellRef={this.cellRef}
|
||||
/>
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
setTimeout(()=>this.load(), 0);
|
||||
}
|
||||
|
||||
interval;
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if(this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
load(){
|
||||
let users = [];
|
||||
|
||||
let rndFirstNames = ['Loes', 'Johnny', 'Maria', 'Jezus', 'Philippe', 'Filip', 'Annie'];
|
||||
let rndLastNames = ['Peeters', 'Wachters', 'Jannsens', 'De Schaetzen', 'Becks', 'Konings', 'De Clerk'];
|
||||
|
||||
for(let i = 0; i < 10; ++i){
|
||||
let first = rndFirstNames[Math.floor(rndFirstNames.length*Math.random())];
|
||||
let last = rndLastNames[Math.floor(rndLastNames.length*Math.random())];
|
||||
users.push({
|
||||
userId: (Math.random()*99999).toString(36).slice(-6).toUpperCase(),
|
||||
identity: {
|
||||
firstName: first,
|
||||
lastName: last,
|
||||
email: `${first}.${last}@example.com`.toLocaleLowerCase()
|
||||
},
|
||||
cells: []
|
||||
})
|
||||
}
|
||||
|
||||
this.users = users;
|
||||
}
|
||||
|
||||
lastClear = new Date();
|
||||
|
||||
moveUp = (ev, u, d)=>{
|
||||
let data = this.users.slice();
|
||||
let indexOf = this.users.indexOf(u);
|
||||
|
||||
if(indexOf>0) {
|
||||
let [before, after] = data.splice(indexOf - 1, 2);
|
||||
data.splice(indexOf - 1, 0, after, before);
|
||||
console.log(data.map((u,index)=>this.rowKey(u,index)))
|
||||
this.users = data;
|
||||
}
|
||||
console.log(this.users);
|
||||
}
|
||||
|
||||
moveDown = (ev, u, d)=>{
|
||||
let data = this.users.slice();
|
||||
let indexOf = this.users.indexOf(u);
|
||||
|
||||
if(indexOf<(this.users.length-1)) {
|
||||
let [before, after] = data.splice(indexOf, 2);
|
||||
data.splice(indexOf, 0, after, before);
|
||||
console.log(data.map((u,index)=>this.rowKey(u,index)));
|
||||
this.users = data;
|
||||
}
|
||||
console.log(this.users);
|
||||
}
|
||||
}
|
||||
|
||||
document.body.appendChild(render(<style>{style}</style>));
|
||||
document.body.appendChild(render(<TableTester/>));
|
||||
19
examples/table-2/index.scss
Normal file
19
examples/table-2/index.scss
Normal file
@ -0,0 +1,19 @@
|
||||
html{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.center-me{
|
||||
align-self: center;
|
||||
}
|
||||
72
examples/table-2/table-component.jsx
Normal file
72
examples/table-2/table-component.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import {CustomElement, defineElement, Host, prop, state} from "../../packages/csx";
|
||||
import TableComponentStyle from "./table-component.scss";
|
||||
|
||||
let tableId = 0;
|
||||
|
||||
@defineElement("tripto-table")
|
||||
export class TableComponent extends CustomElement {
|
||||
|
||||
#columnDefinitions;
|
||||
|
||||
@prop()
|
||||
set columns(value) {
|
||||
this.#columnDefinitions = value;
|
||||
}
|
||||
|
||||
@state() data;
|
||||
|
||||
@prop()
|
||||
set data(value) {
|
||||
this.data = value;
|
||||
this.rows = new Map();
|
||||
}
|
||||
|
||||
@prop()
|
||||
rowKey = (value,index)=>index;
|
||||
|
||||
@prop()
|
||||
cellRef = ()=>null;
|
||||
|
||||
rows = new Map();
|
||||
|
||||
#tableId = tableId++;
|
||||
render() {
|
||||
console.log(`Table render at for ${this.data?.length??0} rows: ${Date.now()}`);
|
||||
return (
|
||||
<Host>
|
||||
<style>{TableComponentStyle}</style>
|
||||
<style>
|
||||
{this.#columnDefinitions?.map((col, idx) => (
|
||||
`#table_${this.#tableId} .cell.cell_${idx} {` +
|
||||
` flex: ${(col.size ? (`0 0 ${col.size}px`) : `1`)};` +
|
||||
`}`
|
||||
))}
|
||||
</style>
|
||||
<section className="table" id={`table_${this.#tableId}`}>
|
||||
<header>
|
||||
<div className="row">
|
||||
{this.#columnDefinitions.map((col, idx) => (
|
||||
<div className={`cell cell_${idx}`}>
|
||||
{col.headerRender()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
{this.data?.map((dataRow,index) => (
|
||||
<div className="row"
|
||||
key={this.rowKey(dataRow, index)}
|
||||
ref={(el)=>this.rows.set(this.rowKey(dataRow,index), el)}>
|
||||
{this.#columnDefinitions.map((col, idx) => (
|
||||
<div className={`cell cell_${idx}`} key={idx} ref={(el)=>this.cellRef(dataRow,idx,el)}>
|
||||
{col.render(dataRow)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</main>
|
||||
</section>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
32
examples/table-2/table-component.scss
Normal file
32
examples/table-2/table-component.scss
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
.table {
|
||||
--box-color: #a0a0a0;
|
||||
--primary-color: #5f74ff;
|
||||
--table-background: #e4e4f0;
|
||||
--box-border: 1px solid #7d7d7d;
|
||||
|
||||
border: var(--box-border);
|
||||
display: block;
|
||||
|
||||
header {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
header > .row,
|
||||
main > .row {
|
||||
background: var(--table-background);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
line-height: 3em;
|
||||
border-bottom: var(--box-border);
|
||||
|
||||
.cell {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
main > .row {
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
169
jest/components/shadow-dom.test.js
Normal file
169
jest/components/shadow-dom.test.js
Normal file
@ -0,0 +1,169 @@
|
||||
import { render, CustomElement, Host, ShadowDOM, defineElement, state, prop } from "@cerxes/csx";
|
||||
import { testContainer } from "../utils/test-container";
|
||||
import { nextAnimationFrame } from "../utils/next-animation-frame";
|
||||
|
||||
describe("Shadow-DOM tests", () => {
|
||||
/**
|
||||
* Assert that shadow dom behaves as expected
|
||||
*/
|
||||
test("Simple shadow-component", async () => {
|
||||
@defineElement('shadow-component')
|
||||
class ShadowComponent extends CustomElement{
|
||||
@prop()
|
||||
title = 'Content here';
|
||||
|
||||
render(){
|
||||
return (
|
||||
<Host>
|
||||
<ShadowDOM>
|
||||
<div>
|
||||
<h1>{this.title}</h1>
|
||||
<slot />
|
||||
</div>
|
||||
</ShadowDOM>
|
||||
</Host>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let initialVSpec = <ShadowComponent />;
|
||||
let rendered = render(initialVSpec);
|
||||
let container = testContainer(rendered);
|
||||
document.body.appendChild(container);// Components need to be added to the DOM or their connectecCallback will not be called
|
||||
|
||||
// Initial render
|
||||
expect(
|
||||
container.innerHTML
|
||||
).toBe([
|
||||
`<shadow-component>`,
|
||||
`</shadow-component>`,
|
||||
].join(''));
|
||||
|
||||
expect(
|
||||
rendered.shadowRoot.innerHTML
|
||||
).toBe([
|
||||
`<div>`,
|
||||
`<h1>Content here</h1>`,
|
||||
`<slot></slot>`,
|
||||
`</div>`,
|
||||
].join(''));
|
||||
|
||||
// Update behaves as it should
|
||||
let updatedVSpec = (
|
||||
<ShadowComponent title={"New content here"}>
|
||||
<li><ul>contents</ul></li>
|
||||
</ShadowComponent>
|
||||
);
|
||||
render(updatedVSpec, {host: rendered, old: initialVSpec});
|
||||
|
||||
// Wait for it to update
|
||||
await nextAnimationFrame();
|
||||
|
||||
expect(
|
||||
container.innerHTML
|
||||
).toBe([
|
||||
`<shadow-component>`,
|
||||
`<li><ul>contents</ul></li>`,
|
||||
`</shadow-component>`,
|
||||
].join(''));
|
||||
|
||||
expect(
|
||||
rendered.shadowRoot.innerHTML
|
||||
).toBe([
|
||||
`<div>`,
|
||||
`<h1>New content here</h1>`,
|
||||
`<slot></slot>`,
|
||||
`</div>`,
|
||||
].join(''));
|
||||
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
test("Nested shadow-component", async () => {
|
||||
@defineElement('todo-item')
|
||||
class TodoItem extends CustomElement {
|
||||
@prop()
|
||||
get model(){ return this.#model; }
|
||||
set model(value){ this.#model = value; }
|
||||
|
||||
#model;
|
||||
|
||||
render(){
|
||||
return (
|
||||
<Host>
|
||||
<ShadowDOM>
|
||||
<input type="checkbox" checked={ this.model.checked }/>
|
||||
<label>
|
||||
<slot />
|
||||
</label>
|
||||
</ShadowDOM>
|
||||
</Host>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@defineElement('my-todo')
|
||||
class MyTodo extends CustomElement {
|
||||
@state()
|
||||
todos = [
|
||||
{ text: "todo 1", checked: true },
|
||||
{ text: "todo 2", checked: false },
|
||||
];
|
||||
|
||||
rendered = [];
|
||||
|
||||
render(){
|
||||
return (
|
||||
<Host>
|
||||
<h1>Todos</h1>
|
||||
<ul>
|
||||
{this.todos.map((todo,index)=>(
|
||||
<TodoItem model={todo}
|
||||
ref={(el)=>this.rendered[index]=el}
|
||||
>
|
||||
{todo.text}
|
||||
</TodoItem>
|
||||
))}
|
||||
</ul>
|
||||
</Host>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let initialVSpec = <MyTodo />;
|
||||
let rendered = render(initialVSpec);
|
||||
let container = testContainer(rendered);
|
||||
document.body.appendChild(container);// Components need to be added to the DOM or their connectecCallback will not be called
|
||||
|
||||
// Initial render
|
||||
expect(
|
||||
container.innerHTML
|
||||
).toBe([
|
||||
`<my-todo>`,
|
||||
`<h1>Todos</h1>`,
|
||||
`<ul>`,
|
||||
...rendered.todos.map(todo=>(
|
||||
`<todo-item>${todo.text}</todo-item>`
|
||||
)),
|
||||
`</ul>`,
|
||||
`</my-todo>`,
|
||||
].join(''));
|
||||
|
||||
for(let i = 0; i < rendered.todos.length; ++i){
|
||||
let todo = rendered.todos[i];
|
||||
let el = rendered.rendered[i];
|
||||
expect(el).not.toBeUndefined();
|
||||
expect(
|
||||
el.shadowRoot.innerHTML
|
||||
).toBe([
|
||||
`<input type="checkbox"${todo.checked? ' checked=""': ''}>`,
|
||||
`<label>`,
|
||||
`<slot></slot>`,
|
||||
`</label>`
|
||||
].join(''));
|
||||
}
|
||||
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
|
||||
});
|
||||
159
jest/components/svg.test.js
Normal file
159
jest/components/svg.test.js
Normal file
@ -0,0 +1,159 @@
|
||||
import { render, CustomElement, Host, ShadowDOM, defineElement, state, prop } from "@cerxes/csx";
|
||||
import { testContainer } from "../utils/test-container";
|
||||
import { nextAnimationFrame } from "../utils/next-animation-frame";
|
||||
|
||||
|
||||
const svgs = {
|
||||
"/assets/icons/checkbox-checked.svg": [
|
||||
`<svg fill="currentcolor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">`,
|
||||
`<title>checkbox-checked</title>`,
|
||||
`<g><path d="M401.7,411.8c-38,0-76.1,0-114.1,0c-55.6,0-111.2,0-166.8,0c-6.3,0-12.6,0-19,0c-1.1,0-2.3,0-3.4,0 c-0.5,0-1.1,0-1.5,0c-4.5,0.1,4.2,1.3,0,0c-0.9-0.3-2.2-0.8-3.2-0.9c-2.3-0.2,4,2.4,0.6,0.2c-0.2-0.1-3.4-1.9-3.3-2 c0.1-0.2,3.5,3.2,0.1-0.2c-0.6-0.6-1.2-1.2-1.7-1.9c1.8,2.4,2.1,2.7,0.8,0.9c-0.1-0.2-2-3.3-1.9-3.4c0.7-0.4,1.2,4.6,0.4,0.5 c-0.1-0.3-0.9-3.2-0.8-3.2c0.1,0.9,0.2,1.8,0.4,2.6c-0.1-0.9-0.1-1.8-0.1-2.7c0-0.2,0-0.5,0-0.7c0-1.5,0-3.1,0-4.6 c0-7.2,0-14.5,0-21.7c0-56.3,0-112.6,0-168.9c0-25.9,0-51.9,0-77.8c0-7.6,0-15.3,0-22.9c0-1.8,0-3.6,0-5.4c0-0.4,0-0.7,0-1.1 c0-0.5,0-1.1,0-1.5c0-3.3,0.5,1.4-0.3,1.3c0,0,1.1-5.2,1.4-5.1c0.1,0-2.2,4.2,0.2,0.1c2.4-4.1,0,0-0.2-0.2 c-0.1-0.1,2.5-2.6,2.7-2.8c2.9-3.1-3.7,2,0,0c0.7-0.4,1.5-0.9,2.2-1.3c3.6-2.3-4.2,1,0,0c0.3-0.1,3.8-1.1,3.9-1 c-3.1,0.4-3.5,0.4-1.3,0.3c0.5,0,0.9,0,1.4,0c0.9,0,1.8,0,2.6,0c5.9,0,11.8,0,17.7,0c24,0,48,0,72,0c58.1,0,116.3,0,174.4,0 c12.2,0,24.4-0.2,36.6,0c0.5,0,1.2,0,1.4,0c4.5-0.1-4.2-1.3,0,0c0.3,0.1,3.8,0.9,3.8,1.1c0,0.1-4.2-2.2-0.1,0.2 c4.1,2.4,0,0,0.2-0.2c0.1-0.1,2.1,2,2.3,2.2c3.5,3.1-1.2-2.7,0.5,0.5c0.1,0.2,2,3.3,1.9,3.4c-0.3,0.1-1.5-4.5-0.2,0.1 c0.1,0.4,0.2,0.9,0.3,1.3c0.4,2.2,0.4,1.7-0.1-1.3c0.1,0,0.1,2.7,0.1,2.7c0,0.2,0,0.5,0,0.7c0,4.5,0,9.1,0,13.7 c0,53.1,0,106.2,0,159.2c0,42.9,0.2,85.8,0,128.7c0,0.3,0,0.7,0,1c0,0.5,0,1.1,0,1.5c0.1,4.5,1.3-4.2,0,0 c-0.1,0.3-0.9,3.8-1.1,3.8c-0.1,0,2.2-4.2-0.2-0.1c-0.2,0.3-1.3,2.2-1.4,2.2c-0.2-0.1,3.2-3.5-0.2-0.1c-3.3,3.3,0,0,0.1,0.2 c0.1,0.2-3.1,1.9-3.3,2c-3.6,2.3,4.2-1,0,0c-0.3,0.1-3.8,1.1-3.9,1c0.9-0.1,1.8-0.2,2.6-0.4C403.5,411.7,402.6,411.8,401.7,411.8 c-10.5,0.2-20.5,9-20,20c0.5,10.6,8.8,20.3,20,20c27.8-0.6,49.6-22.6,50.1-50.3c0.1-4.1,0-8.3,0-12.4c0-22.3,0-44.6,0-67 c0-60,0-120.1,0-180.1c0-14.4,0.2-28.8,0-43.2c-0.3-23.1-16.1-44.8-39.4-49.4c-6.3-1.3-12.3-1.2-18.7-1.2 c-51.3,0-102.5,0-153.8,0c-46.5,0-93,0-139.5,0c-17.6,0-33,7.3-43.3,21.6c-7,9.7-9,21.1-9,32.8c0,18.3,0,36.6,0,54.8 c0,60.9,0,121.8,0,182.8c0,18.8,0,37.6,0,56.5c0,10.2,1.3,20,6.2,29.2c8.9,16.8,27,25.9,45.6,25.9c45.4,0.1,90.7,0,136.1,0 c52.2,0,104.4,0,156.7,0c3,0,6,0,9,0c10.5,0,20.5-9.2,20-20C421.2,420.9,412.9,411.8,401.7,411.8z"></path></g>`,
|
||||
`<g><path d="M136.9,276.6c20,18,40,36,60.1,54.1c8.1,7.3,20.2,7.8,28.3,0c15.6-15,31.1-30,46.7-45.1 c24.7-23.8,49.4-47.7,74.1-71.5c5.7-5.5,11.4-11,17.2-16.6c7.5-7.3,7.9-21.1,0-28.3c-8.1-7.4-20.2-7.8-28.3,0 c-15.6,15-31.1,30-46.7,45.1c-24.7,23.8-49.4,47.7-74.1,71.5c-5.7,5.5-11.4,11-17.2,16.6c9.4,0,18.9,0,28.3,0 c-20-18-40-36-60.1-54.1c-7.8-7-20.7-8.2-28.3,0C129.9,256,128.6,269.2,136.9,276.6L136.9,276.6z"></path></g>`,
|
||||
`</svg>`
|
||||
].join(''),
|
||||
"/assets/icons/checkbox.svg": [
|
||||
`<svg fill="currentcolor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">`,
|
||||
`<title>checkbox</title>`,
|
||||
`<g><path d="M401.7,411.8c-38,0-76.1,0-114.1,0c-55.6,0-111.2,0-166.8,0c-6.3,0-12.6,0-19,0c-1.1,0-2.3,0-3.4,0 c-0.5,0-1.1,0-1.5,0c-4.5,0.1,4.2,1.3,0,0c-0.9-0.3-2.2-0.8-3.2-0.9c-2.3-0.2,4,2.4,0.6,0.2c-0.2-0.1-3.4-1.9-3.3-2 c0.1-0.2,3.5,3.2,0.1-0.2c-0.6-0.6-1.2-1.2-1.7-1.9c1.8,2.4,2.1,2.7,0.8,0.9c-0.1-0.2-2-3.3-1.9-3.4c0.7-0.4,1.2,4.6,0.4,0.5 c-0.1-0.3-0.9-3.2-0.8-3.2c0.1,0.9,0.2,1.8,0.4,2.6c-0.1-0.9-0.1-1.8-0.1-2.7c0-0.2,0-0.5,0-0.7c0-1.5,0-3.1,0-4.6 c0-7.2,0-14.5,0-21.7c0-56.3,0-112.6,0-168.9c0-25.9,0-51.9,0-77.8c0-7.6,0-15.3,0-22.9c0-1.8,0-3.6,0-5.4c0-0.4,0-0.7,0-1.1 c0-0.5,0-1.1,0-1.5c0-3.3,0.5,1.4-0.3,1.3c0,0,1.1-5.2,1.4-5.1c0.1,0-2.2,4.2,0.2,0.1c2.4-4.1,0,0-0.2-0.2 c-0.1-0.1,2.5-2.6,2.7-2.8c2.9-3.1-3.7,2,0,0c0.7-0.4,1.5-0.9,2.2-1.3c3.6-2.3-4.2,1,0,0c0.3-0.1,3.8-1.1,3.9-1 c-3.1,0.4-3.5,0.4-1.3,0.3c0.5,0,0.9,0,1.4,0c0.9,0,1.8,0,2.6,0c5.9,0,11.8,0,17.7,0c24,0,48,0,72,0c58.1,0,116.3,0,174.4,0 c12.2,0,24.4-0.2,36.6,0c0.5,0,1.2,0,1.4,0c4.5-0.1-4.2-1.3,0,0c0.3,0.1,3.8,0.9,3.8,1.1c0,0.1-4.2-2.2-0.1,0.2 c4.1,2.4,0,0,0.2-0.2c0.1-0.1,2.1,2,2.3,2.2c3.5,3.1-1.2-2.7,0.5,0.5c0.1,0.2,2,3.3,1.9,3.4c-0.3,0.1-1.5-4.5-0.2,0.1 c0.1,0.4,0.2,0.9,0.3,1.3c0.4,2.2,0.4,1.7-0.1-1.3c0.1,0,0.1,2.7,0.1,2.7c0,0.2,0,0.5,0,0.7c0,4.5,0,9.1,0,13.7 c0,53.1,0,106.2,0,159.2c0,42.9,0.2,85.8,0,128.7c0,0.3,0,0.7,0,1c0,0.5,0,1.1,0,1.5c0.1,4.5,1.3-4.2,0,0 c-0.1,0.3-0.9,3.8-1.1,3.8c-0.1,0,2.2-4.2-0.2-0.1c-0.2,0.3-1.3,2.2-1.4,2.2c-0.2-0.1,3.2-3.5-0.2-0.1c-3.3,3.3,0,0,0.1,0.2 c0.1,0.2-3.1,1.9-3.3,2c-3.6,2.3,4.2-1,0,0c-0.3,0.1-3.8,1.1-3.9,1c0.9-0.1,1.8-0.2,2.6-0.4C403.5,411.7,402.6,411.8,401.7,411.8 c-10.5,0.2-20.5,9-20,20c0.5,10.6,8.8,20.3,20,20c27.8-0.6,49.6-22.6,50.1-50.3c0.1-4.1,0-8.3,0-12.4c0-22.3,0-44.6,0-67 c0-60,0-120.1,0-180.1c0-14.4,0.2-28.8,0-43.2c-0.3-23.1-16.1-44.8-39.4-49.4c-6.3-1.3-12.3-1.2-18.7-1.2c-51.3,0-102.5,0-153.8,0 c-46.5,0-93,0-139.5,0c-17.6,0-33,7.3-43.3,21.6c-7,9.7-9,21.1-9,32.8c0,18.3,0,36.6,0,54.8c0,60.9,0,121.8,0,182.8 c0,18.8,0,37.6,0,56.5c0,10.2,1.3,20,6.2,29.2c8.9,16.8,27,25.9,45.6,25.9c45.4,0.1,90.7,0,136.1,0c52.2,0,104.4,0,156.7,0 c3,0,6,0,9,0c10.5,0,20.5-9.2,20-20C421.2,420.9,412.9,411.8,401.7,411.8z"></path></g>`,
|
||||
`</svg>`
|
||||
].join('')
|
||||
};
|
||||
|
||||
const iconStyle = `:host { display: inline-block; }`;// The rest is left out
|
||||
|
||||
|
||||
describe("SVG Component tests", () => {
|
||||
/**
|
||||
* Assert that a basic component renders as expected
|
||||
*/
|
||||
test("Simple example-component", async () => {
|
||||
|
||||
let svgCache = new Map();
|
||||
async function fetchSvg(svgUrl){
|
||||
// Fake timer
|
||||
await new Promise((resolve,reject)=>setTimeout(()=>resolve(), 1));
|
||||
let svg = svgs[svgUrl];
|
||||
if(svg){
|
||||
return {
|
||||
text: ()=>svg
|
||||
}
|
||||
}else{
|
||||
throw new Error("Not found");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} svgUrl
|
||||
* @returns {Promise<Element>}
|
||||
*/
|
||||
async function loadSvg(svgUrl) {
|
||||
const response = await fetchSvg(svgUrl);
|
||||
const svgResource = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const svgDocument = parser.parseFromString(svgResource, "image/svg+xml");
|
||||
let svgElement = svgDocument.firstElementChild;
|
||||
if (svgElement.hasAttribute("fill")) {
|
||||
svgElement.setAttribute("fill", "currentcolor")
|
||||
}
|
||||
svgCache.set(svgUrl, svgElement);
|
||||
return svgElement;
|
||||
}
|
||||
|
||||
@defineElement("test-icon")
|
||||
class Icon extends CustomElement {
|
||||
// Again JEST fucks this up in that these magically become read-only (probably not using our CSX-version of babel!)
|
||||
@state()
|
||||
set svgElement(value){ this.#svgElement = value};
|
||||
get svgElement(){ return this.#svgElement};
|
||||
#svgElement;
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} icon
|
||||
*/
|
||||
@prop() set icon(icon) {
|
||||
if(icon !== this.#icon) {
|
||||
this.#icon = icon;
|
||||
|
||||
this.updateIcon();
|
||||
}
|
||||
};
|
||||
get icon(){
|
||||
return this.#icon;
|
||||
}
|
||||
#icon;
|
||||
|
||||
updateIcon(){
|
||||
let icon = this.#icon;
|
||||
const svgUrl = `/assets/icons/${icon}.svg`;
|
||||
let cached = svgCache.get(svgUrl);
|
||||
if(cached){
|
||||
// Use from cache (without passing by async functions, to optimize rendering loop!)
|
||||
this.svgElement = cached.cloneNode(true);
|
||||
}else{
|
||||
loadSvg(svgUrl).then(svgEl=> {
|
||||
if(icon===this.#icon) {
|
||||
// If this is still the desired icon, load it
|
||||
this.svgElement = svgEl.cloneNode(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Host>
|
||||
<ShadowDOM>
|
||||
<style>{iconStyle}</style>
|
||||
<div className="icon">
|
||||
{this.svgElement}
|
||||
</div>
|
||||
</ShadowDOM>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let initialVSpec = <Icon icon={'checkbox'}/>
|
||||
let rendered = render(initialVSpec);
|
||||
let container = testContainer(rendered);
|
||||
|
||||
document.body.appendChild(container);// Components need to be added to the DOM or their connectecCallback will not be called
|
||||
|
||||
expect(
|
||||
container.innerHTML
|
||||
).toBe([
|
||||
`<test-icon>`,
|
||||
`</test-icon>`,
|
||||
].join(''));
|
||||
|
||||
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),10));
|
||||
await nextAnimationFrame();
|
||||
|
||||
expect(
|
||||
rendered.shadowRoot.innerHTML
|
||||
).toBe([
|
||||
`<style>${iconStyle}</style>`,
|
||||
`<div class="icon">`,
|
||||
svgs["/assets/icons/checkbox.svg"],
|
||||
`</div>`
|
||||
].join(''));
|
||||
|
||||
let updatedVSpec = <Icon icon={'checkbox-checked'}/>;
|
||||
render(updatedVSpec, {host: rendered, old: initialVSpec});
|
||||
|
||||
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),10));
|
||||
await nextAnimationFrame();
|
||||
|
||||
expect(
|
||||
rendered.shadowRoot.innerHTML
|
||||
).toBe([
|
||||
`<style>${iconStyle}</style>`,
|
||||
`<div class="icon">`,
|
||||
svgs["/assets/icons/checkbox-checked.svg"],
|
||||
`</div>`
|
||||
].join(''));
|
||||
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
});
|
||||
@ -194,13 +194,14 @@ describe("Children", () => {
|
||||
].join(''));
|
||||
});
|
||||
|
||||
test("Nulls are ignored", async () => {
|
||||
test("Nulls and undefined are ignored", async () => {
|
||||
expect(
|
||||
testContainer(
|
||||
render(
|
||||
<div class="container">
|
||||
<h1>Title</h1>
|
||||
{null}
|
||||
{undefined}
|
||||
<button>Save</button>
|
||||
</div>
|
||||
)
|
||||
@ -212,4 +213,50 @@ describe("Children", () => {
|
||||
`</div>`
|
||||
].join(''));
|
||||
});
|
||||
|
||||
test("Update maintains the same elements where possible", async () => {
|
||||
let initialVSpec = (
|
||||
<div class="container">
|
||||
<h1>Title</h1>
|
||||
<button>Save</button>
|
||||
</div>
|
||||
);
|
||||
let rendered = render(initialVSpec);
|
||||
let container = testContainer(rendered);
|
||||
|
||||
let children = Array.from(rendered.childNodes);// Capture current child-nodes
|
||||
|
||||
expect(
|
||||
container.innerHTML
|
||||
).toBe([
|
||||
`<div class="container">`,
|
||||
`<h1>Title</h1>`,
|
||||
`<button>Save</button>`,
|
||||
`</div>`
|
||||
].join(''));
|
||||
|
||||
// Update it
|
||||
let updatedVSpec = (
|
||||
<div class="container">
|
||||
<h1>Update</h1>
|
||||
<button>Dismiss</button>
|
||||
</div>
|
||||
);
|
||||
render(updatedVSpec, {host: rendered, old: initialVSpec});
|
||||
expect(
|
||||
container.innerHTML
|
||||
).toBe([
|
||||
`<div class="container">`,
|
||||
`<h1>Update</h1>`,
|
||||
`<button>Dismiss</button>`,
|
||||
`</div>`
|
||||
].join(''));
|
||||
|
||||
let updatedChildren = Array.from(rendered.childNodes);// Capture current child-nodes
|
||||
|
||||
expect(children.length).toBe(updatedChildren.length);
|
||||
for(let i = 0; i < children.length; ++i){
|
||||
expect(children[i] === updatedChildren[i]).toBe(true);// Expect the element to be the same by ref
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -3,14 +3,14 @@ import { testContainer } from "../utils/test-container";
|
||||
|
||||
describe("Key-property tests", () => {
|
||||
test("Keyed list", async () => {
|
||||
let renderedIndexes = [];
|
||||
let initIndexes = [0, 1, 2, 3];
|
||||
let initialRendered = new Map();
|
||||
let initIndexes = [1, 2, 3, 4];
|
||||
|
||||
let makeSpec = (targetList, indexes)=>(
|
||||
<ul>
|
||||
{
|
||||
indexes.map(index => (
|
||||
<li id={`li_${index}`} key={index} ref={(el) => targetList[ index ] = el}>
|
||||
<li id={`li_${index}`} key={index} ref={(el) => targetList.set(index, el)}>
|
||||
{index}
|
||||
</li>
|
||||
))
|
||||
@ -18,7 +18,7 @@ describe("Key-property tests", () => {
|
||||
</ul>
|
||||
);
|
||||
|
||||
let initialVSpec = makeSpec(renderedIndexes, initIndexes);
|
||||
let initialVSpec = makeSpec(initialRendered, initIndexes);
|
||||
|
||||
let rendered = render(initialVSpec);
|
||||
let container = testContainer(rendered);
|
||||
@ -31,17 +31,18 @@ describe("Key-property tests", () => {
|
||||
].join('')
|
||||
);
|
||||
|
||||
expect(renderedIndexes.length).toBe(4);
|
||||
for(let rendered of renderedIndexes){
|
||||
expect(initialRendered.size).toBe(4);
|
||||
for(let rendered of initialRendered){
|
||||
expect(rendered).not.toBeUndefined();
|
||||
}
|
||||
|
||||
let reorderedIndexes = [3,2,1,0];
|
||||
let rerenderedIndexes = renderedIndexes.slice();
|
||||
// Reverse order
|
||||
let reorderedIndexes = [4,3,2,1];
|
||||
let rerenderedIndexes = new Map(initialRendered);
|
||||
let updatedVSpec = makeSpec(rerenderedIndexes, reorderedIndexes);
|
||||
render(updatedVSpec, {host: rendered, old: initialVSpec});// Is this host right? it seems inconsistent and the source of our bug (as it is probably also misused in custom-elements)
|
||||
render(updatedVSpec, {host: rendered, old: initialVSpec});
|
||||
|
||||
// Updated
|
||||
// Updated (reverse order)
|
||||
expect(container.innerHTML).toBe(
|
||||
[
|
||||
`<ul>`,
|
||||
@ -51,11 +52,46 @@ describe("Key-property tests", () => {
|
||||
);
|
||||
|
||||
// Validate that items were merely re-arranged and not re-created
|
||||
expect(rerenderedIndexes.length).toBe(4);
|
||||
for(let i=0; i<4; ++i){
|
||||
let initRendered = renderedIndexes[i];
|
||||
let reorderedRendered = rerenderedIndexes[i];
|
||||
expect(rerenderedIndexes.size).toBe(4);
|
||||
for(let i of initIndexes){
|
||||
let initRendered = initialRendered.get(i);
|
||||
let reorderedRendered = rerenderedIndexes.get(i);
|
||||
expect(initRendered === reorderedRendered).toBe(true); // These should've remained the same
|
||||
}
|
||||
|
||||
// Add items and change order
|
||||
let additionalIndexes = [0, 1, 2.5, 2, 3, 4, 5.5];
|
||||
let additionalRerenderedIndexes = new Map(initialRendered);
|
||||
let secondUpdatedVSpec = makeSpec(additionalRerenderedIndexes, additionalIndexes);
|
||||
render(secondUpdatedVSpec, {host: rendered, old: updatedVSpec});// Is this host right? it seems inconsistent and the source of our bug (as it is probably also misused in custom-elements)
|
||||
|
||||
// Validate add items and changed order
|
||||
expect(container.innerHTML).toBe(
|
||||
[
|
||||
`<ul>`,
|
||||
...additionalIndexes.map(index=>`<li id="${`li_${index}`}">${index}</li>`),
|
||||
`</ul>`
|
||||
].join('')
|
||||
);
|
||||
|
||||
// Validate that items were merely re-arranged and not re-created
|
||||
expect(additionalRerenderedIndexes.size).toBe(additionalIndexes.length);
|
||||
for(let i of initIndexes){
|
||||
let initRendered = initialRendered.get(i);
|
||||
let additionalRendered = additionalRerenderedIndexes.get(i);
|
||||
expect(initRendered === additionalRendered).toBe(true); // These should've still remained the same
|
||||
}
|
||||
|
||||
// Revert back to the original
|
||||
render(initialVSpec, {host: rendered, old: secondUpdatedVSpec});// Is this host right? it seems inconsistent and the source of our bug (as it is probably also misused in custom-elements)
|
||||
|
||||
// Validate reverting back to the original
|
||||
expect(container.innerHTML).toBe(
|
||||
[
|
||||
`<ul>`,
|
||||
...initIndexes.map(index=>`<li id="${`li_${index}`}">${index}</li>`),
|
||||
`</ul>`
|
||||
].join('')
|
||||
);
|
||||
});
|
||||
});
|
||||
62
jest/render/svg-rendering.test.js
Normal file
62
jest/render/svg-rendering.test.js
Normal file
@ -0,0 +1,62 @@
|
||||
import { render } from "@cerxes/csx";
|
||||
import { testContainer } from "../utils/test-container";
|
||||
|
||||
describe("SVG-rendering test", () => {
|
||||
test("Simple", async () => {
|
||||
let makeSpec = (stroke, strokeWidth) => (
|
||||
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg"
|
||||
stroke={stroke}>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(1 1)" stroke-width={strokeWidth}>
|
||||
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
|
||||
<path d="M36 18c0-9.94-8.06-18-18-18">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 18 18"
|
||||
to="360 18 18"
|
||||
dur="1s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
let initialVSpec = makeSpec("#000", 2);
|
||||
let rendered = render(initialVSpec);
|
||||
let container = testContainer(rendered);
|
||||
|
||||
expect(container.innerHTML).toBe([
|
||||
`<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#000">`,
|
||||
`<g fill="none" fill-rule="evenodd">`,
|
||||
`<g transform="translate(1 1)" stroke-width="2">`,
|
||||
`<circle stroke-opacity=".5" cx="18" cy="18" r="18">`,
|
||||
`</circle>`,
|
||||
`<path d="M36 18c0-9.94-8.06-18-18-18">`,
|
||||
`<animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite">`,
|
||||
`</animateTransform>`,
|
||||
`</path>`,
|
||||
`</g>`,
|
||||
`</g>`,
|
||||
`</svg>`
|
||||
].join(''));
|
||||
|
||||
let updatedVSpec = makeSpec("#FFF", 4);
|
||||
render(updatedVSpec, { host: rendered, old: initialVSpec });
|
||||
|
||||
expect(container.innerHTML).toBe([
|
||||
`<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#FFF">`,
|
||||
`<g fill="none" fill-rule="evenodd">`,
|
||||
`<g transform="translate(1 1)" stroke-width="4">`,
|
||||
`<circle stroke-opacity=".5" cx="18" cy="18" r="18">`,
|
||||
`</circle>`,
|
||||
`<path d="M36 18c0-9.94-8.06-18-18-18">`,
|
||||
`<animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite">`,
|
||||
`</animateTransform>`,
|
||||
`</path>`,
|
||||
`</g>`,
|
||||
`</g>`,
|
||||
`</svg>`
|
||||
].join(''));
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,4 @@
|
||||
# Don't publish the src containing ESNext proposal's code. Only publish the bundled output in dist/ and the ES6-transpiled src from lib/
|
||||
src/*
|
||||
node_modules/*
|
||||
rollup.config.js
|
||||
yarn.lock
|
||||
@ -110,7 +110,7 @@ export class CsxConfig {
|
||||
let srcOpts = {
|
||||
es: format==='es',
|
||||
single: type==='lib',
|
||||
sourcemap: true,// Just always there for now
|
||||
sourcemap: minified ? true : (format==='es'? false : true),
|
||||
minified: !!minified
|
||||
};
|
||||
let outDir = type==='lib'? lib : dist;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@cerxes/csx",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.13",
|
||||
"author": "Miel Truyen <miel.truyen@cerxes.net>",
|
||||
"description": "CSX is a minimalistic UI-framework inspired by React+JSX for usage with WebComponents.",
|
||||
"repository": {
|
||||
|
||||
58
packages/csx/src/vdom/node-meta.js
Normal file
58
packages/csx/src/vdom/node-meta.js
Normal file
@ -0,0 +1,58 @@
|
||||
import './types';
|
||||
import {
|
||||
HostNodeRenderer, Host,
|
||||
ShadowNodeRenderer, ShadowDOM,
|
||||
PrimitiveRenderer, Primitive,
|
||||
NodeTreeRenderer, NativeRenderer
|
||||
} from "./renderers";
|
||||
|
||||
/**
|
||||
* Meta data used to handle a node in the render lope
|
||||
* @typedef {VNodeRendererMeta} VNodeMeta
|
||||
* @category VDOM.renderer
|
||||
* @property {VNodeRenderer} renderer - The renderer to use to update, create or delete this node
|
||||
* @property {VNodeType} normedType - Normed type of the node
|
||||
**/
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param vnode
|
||||
* @returns {VNodeMeta|undefined}
|
||||
*/
|
||||
export function getNodeMeta(vnode) {
|
||||
if (vnode === undefined || vnode === null) return undefined; // Indicate it shouldn't render
|
||||
if (vnode instanceof Node){
|
||||
return {
|
||||
...NativeRenderer.meta(),
|
||||
renderer: NativeRenderer,
|
||||
normedType: Node
|
||||
};
|
||||
}
|
||||
let type = vnode?.type;
|
||||
if (!type){
|
||||
return {
|
||||
...PrimitiveRenderer.meta(),
|
||||
renderer: PrimitiveRenderer,
|
||||
normedType: Primitive
|
||||
};
|
||||
} else if (type === Host){
|
||||
return {
|
||||
...HostNodeRenderer.meta(),
|
||||
renderer: HostNodeRenderer,
|
||||
normedType: Host
|
||||
};
|
||||
} else if (type === ShadowDOM){
|
||||
return {
|
||||
...ShadowNodeRenderer.meta(),
|
||||
renderer: ShadowNodeRenderer,
|
||||
normedType: ShadowDOM
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...NodeTreeRenderer.meta(),
|
||||
renderer: NodeTreeRenderer,
|
||||
normedType: window.customElements?.get(type) ?? type
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,71 +1,53 @@
|
||||
import './types';
|
||||
import {
|
||||
HostNodeRenderer, Host,
|
||||
ShadowNodeRenderer, ShadowDOM,
|
||||
PrimitiveRenderer, Primitive,
|
||||
NodeTreeRenderer, NativeRenderer
|
||||
} from "./renderers";
|
||||
|
||||
export function getNodeMeta(vnode) {
|
||||
if (vnode === undefined || vnode === null) return undefined; // Indicate it shouldn't render
|
||||
if (vnode instanceof Node) return { renderer: NativeRenderer, normedType: Node };
|
||||
let type = vnode?.type;
|
||||
if (!type) return { renderer: PrimitiveRenderer, normedType: Primitive };
|
||||
else if (type === Host) return { renderer: HostNodeRenderer, normedType: Host };
|
||||
else if (type === ShadowDOM) return { renderer: ShadowNodeRenderer, normedType: ShadowDOM };
|
||||
else return { renderer: NodeTreeRenderer, normedType: window.customElements?.get(type) ?? type };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} RenderOptions
|
||||
* @category VDOM
|
||||
* @property {Element} [host] - The element to update to the specified VDOM
|
||||
* @property {VNode} [old] - Old VNode representation of rendered host
|
||||
* @property {Document} [document] - The document we're rendering to
|
||||
* @property {Element} [parent] - The parent element (TODO not sure what this will do when specified; Insert it as child element of the parent where?)
|
||||
*/
|
||||
import { getNodeMeta } from "./node-meta";
|
||||
|
||||
/**
|
||||
* This exists as a very basic example/test for JSX-to-DOM
|
||||
* @category VDOM
|
||||
* @param {VNode} vnode
|
||||
* @param {RenderOptions} opts
|
||||
* @param {RenderOptions} [opts]
|
||||
* @param {Element} [opts.host] - The element to update to the specified VDOM
|
||||
* @param {VNode} [opts.old] - Old VNode representation of rendered host
|
||||
* @param {Document} [opts.document] - The document we're rendering to
|
||||
* @return {Element}
|
||||
*/
|
||||
export function render(vnode, opts = {}) {
|
||||
// TODO this code could use restructuring when opts.host and vnode.type are incompatible (non updatable type), the host element should be replaced
|
||||
// with a newly created element, like it does with all child-elements..
|
||||
// General flow of this code is to process the hierarchy using a queue (so no recursion is used)
|
||||
// on each node of the hierarchy a renderer is determined which is compared to the renderer of the previous version of this vnode-hierarchy
|
||||
// to determine if these nodes can be updated (e.g updating a div, or textnode) and if they behave as a child-node (e.g. shows up in childNodes)
|
||||
// or are some other special type of node (like Host or ShadowDOM)
|
||||
/**
|
||||
*
|
||||
* @type {VRenderState}
|
||||
*/
|
||||
/** @type {VRenderState} */
|
||||
let state = {
|
||||
keyedElements: new Map(),
|
||||
refs: [],
|
||||
queue: [{
|
||||
// Start item
|
||||
item: {
|
||||
document: opts.document || document,
|
||||
host: opts.host,
|
||||
parent: opts.parent,
|
||||
old: opts.old,
|
||||
vnode: vnode
|
||||
},
|
||||
meta: getNodeMeta(vnode)
|
||||
meta: getNodeMeta(vnode),
|
||||
parent: null,
|
||||
attached: true// Root item is treated as if it is attached
|
||||
}]
|
||||
};
|
||||
|
||||
let newRoot = undefined;
|
||||
while (state.queue.length > 0) {
|
||||
let { item, meta, previous } = state.queue.splice(0, 1)[ 0 ];
|
||||
/** @type {VRenderQueueItem} */
|
||||
let queueItem = state.queue.shift();
|
||||
let {
|
||||
/** @type {VRenderItem} */ item,
|
||||
/** @type {VNodeMeta} */ meta,
|
||||
/** @type {VRenderItem} */ previousNode,
|
||||
/** @type {boolean} */ attached
|
||||
} = queueItem;
|
||||
/** @type {VNodeRenderer} */
|
||||
let renderer = meta.renderer;
|
||||
if (!renderer) throw new Error("No renderer for vnode", item.vnode);
|
||||
|
||||
// SVG handling..
|
||||
// SVG handling.. (this could be improved)
|
||||
if (!item.inSvg && item.vnode?.type === 'svg') item.inSvg = true;
|
||||
else if (item.inSvg && item.vnode?.type === 'foreignObject') item.inSvg = false;
|
||||
|
||||
@ -74,8 +56,9 @@ export function render(vnode, opts = {}) {
|
||||
if (!item.host) {
|
||||
item.host = renderer.create(item, meta);
|
||||
newlyCreated = true;
|
||||
|
||||
if (item.vnode?.props?.ref) {// If props specify a ref-function, queue it to be called at the end of the render
|
||||
|
||||
// If props specify a ref-function, queue it to be called at the end of the render
|
||||
if (item.vnode?.props?.ref) {
|
||||
state.refs.push([item.vnode.props.ref, item.host]);
|
||||
}
|
||||
}
|
||||
@ -84,180 +67,166 @@ export function render(vnode, opts = {}) {
|
||||
renderer.update(item, meta);
|
||||
|
||||
// Update children
|
||||
if (meta.normedType !== Node && (item.vnode?.children || item.old?.children)) {
|
||||
let childTypes = new Set();
|
||||
if (meta.hasChildren) {
|
||||
/** @type {FlattenedVNode[]} */
|
||||
let vChildren = flattenVNodeChildren(item.vnode);
|
||||
/** @type {FlattenedOldVNode[]} */
|
||||
let oldVChildren = flattenVNodeChildren(item?.old);
|
||||
/** @type {Map<*, FlattenedOldVNode>} */
|
||||
let keyedChildren = new Map();// Old child-items
|
||||
/** @type {NodeState[]} */
|
||||
let nodes = Array.from(item.host.childNodes).map(node=>({node, attached: true}));
|
||||
/** @type {FlattenedOldVNode[]} */
|
||||
let specialOldVChildren = []; // List of old-vnodes that are not node-type node and have no key
|
||||
|
||||
// Flatten and organize new vNode-children (this could be a separate function, or implemented using a helper function (because mucht of the code is similar between old/new vnodes)
|
||||
/**
|
||||
* @type { Object.<VNodeType, Array.<VRenderQueueItem>> }
|
||||
*/
|
||||
let vChildren = {};
|
||||
let queue = (item.vnode?.children || []).slice();
|
||||
while (queue.length > 0) {
|
||||
let next = queue.splice(0, 1)[ 0 ];
|
||||
if (next instanceof Array) queue.splice(0, 0, ...next);
|
||||
else {
|
||||
let meta = getNodeMeta(next);
|
||||
if (meta && meta.renderer) {
|
||||
// Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
|
||||
let childType = meta.normedType;
|
||||
if (!meta.renderer.remove) childType = 'node'; // Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
|
||||
childTypes.add(childType);// Track that children of this type exist and should be iterated later
|
||||
vChildren[ childType ] = vChildren[ childType ] || []; // Make sure the array exists
|
||||
vChildren[ childType ].push({
|
||||
item: {
|
||||
...item,
|
||||
old: undefined,
|
||||
vnode: next,
|
||||
host: undefined,
|
||||
parent: item.host
|
||||
},
|
||||
meta: meta
|
||||
});
|
||||
}
|
||||
// Iterate old children and link them to their corresponding elements
|
||||
let nodeI = 0, nodesLength = nodes.length;
|
||||
let oldVItemI = 0, oldVItemLength = oldVChildren.length;
|
||||
while(oldVItemI < oldVItemLength){
|
||||
/** @type {FlattenedOldVNode} */
|
||||
let oldVItem = oldVChildren[oldVItemI];
|
||||
/** @type {NodeState} */
|
||||
let node = nodes[nodeI];
|
||||
let nodeKey = oldVItem.vnode?.props?.key
|
||||
if(nodeKey !== undefined){
|
||||
keyedChildren.set(nodeKey, oldVItem);
|
||||
}
|
||||
++oldVItemI;
|
||||
if(oldVItem.meta.isNode && node){
|
||||
oldVItem.node = node;
|
||||
node.old = oldVItem;
|
||||
++nodeI;
|
||||
}else if(nodeKey === undefined){
|
||||
specialOldVChildren.push(oldVItem);
|
||||
}
|
||||
}
|
||||
// Any items not part of the old-vnode spec should be left alone, and ignored in further processing
|
||||
nodesLength = nodeI; // TODO this feels dirty, but not having this here will screw up our examples, we have yet to add a proper test
|
||||
|
||||
// Flatten and organize old-children
|
||||
/**
|
||||
* @type { Object.<VNodeType, Array.<VOldQueueItem>> }
|
||||
*/
|
||||
let oldVChildren = {};
|
||||
let curElement = item.host.firstChild;
|
||||
queue = (item.old?.children || []).slice();
|
||||
while (queue.length > 0) {
|
||||
let next = queue.splice(0, 1)[ 0 ];
|
||||
if (next instanceof Array) queue.splice(0, 0, ...next);
|
||||
else {
|
||||
let meta = getNodeMeta(next);
|
||||
if (meta && meta.renderer) {
|
||||
// Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
|
||||
let childType = meta.normedType;
|
||||
let childElement;
|
||||
if (!meta.renderer.remove) {
|
||||
childType = 'node';// Treat anything that doesnt have a special remove-function as ChildNode-type (e.g. it shows up in Element.childNodes)
|
||||
if (curElement) {
|
||||
childElement = curElement;
|
||||
curElement = curElement.nextSibling;
|
||||
}
|
||||
}
|
||||
childTypes.add(childType);// Track that children of this type exist and should be iterated later
|
||||
oldVChildren[ childType ] = oldVChildren[ childType ] || []; // Make sure the array exists
|
||||
let oldItem = {
|
||||
vnode: next,
|
||||
element: childElement,
|
||||
meta: meta
|
||||
};
|
||||
oldVChildren[ childType ].push(oldItem);
|
||||
if (next.props?.key) {
|
||||
state.keyedElements.set(next.props?.key, oldItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sortedChildTypes = Array.from(childTypes).sort((a, b) => a === 'node' ? 1 : -1); // Always do ChildNode-types last
|
||||
let queuedItems = [];
|
||||
/**@type {VRenderQueueItem}*/ let previous = null;
|
||||
for (let childType of sortedChildTypes) {
|
||||
let newChildren = vChildren[ childType ];
|
||||
let oldChildren = oldVChildren[ childType ];
|
||||
// Iterate new children, remove old nodes as needed and create new queue items
|
||||
/** @type {VRenderQueueItem[]} */
|
||||
let queueChildren = [];
|
||||
/** @type {VRenderItem} */
|
||||
let previousNodeItem = undefined;
|
||||
let vItemI = 0, vItemLength = vChildren.length;
|
||||
nodeI = 0;
|
||||
while(vItemI < vItemLength || nodeI < nodesLength){
|
||||
let vItem = vChildren[vItemI];
|
||||
let node = nodes[nodeI];
|
||||
|
||||
while (newChildren && newChildren.length) {
|
||||
let child = newChildren.splice(0, 1)[ 0 ];
|
||||
if(vItem){
|
||||
let nodeKey = vItem?.vnode?.props?.key;
|
||||
/** @type {FlattenedOldVNode} */
|
||||
let oldItem = undefined;
|
||||
if(nodeKey!==undefined){
|
||||
oldItem = keyedChildren.get(nodeKey);
|
||||
}
|
||||
|
||||
// Key handling
|
||||
let childKey = child.item.vnode.props?.key;
|
||||
/**@type {VOldQueueItem}*/ let oldChild;
|
||||
if (childKey) {
|
||||
oldChild = state.keyedElements.get(childKey);
|
||||
if (oldChild) {
|
||||
if (oldChildren && oldChildren[ 0 ] === oldChild) {
|
||||
// Old keyed child already in the right place (just clear it from the queue);
|
||||
oldChildren.splice(0, 1);
|
||||
/** @type {NodeState} */
|
||||
let matchedNode = undefined;
|
||||
|
||||
// If this is a DOM-node-type match it to an existing node
|
||||
if(vItem.meta.isNode) {
|
||||
// Match new item to existing node when possible
|
||||
matchedNode = oldItem?.node;
|
||||
if (matchedNode) {
|
||||
// DOM-node matched by key
|
||||
if (matchedNode === node) {
|
||||
// Item in order with old representation
|
||||
++nodeI;
|
||||
} else {
|
||||
// Old keyed child not already in the right place
|
||||
let indexOfKeyed = oldChildren.indexOf(oldChild);
|
||||
if (indexOfKeyed) {
|
||||
oldChildren.splice(indexOfKeyed, 1);
|
||||
if(oldChild.element) {
|
||||
item.host.removeChild(oldChild.element);
|
||||
}else{
|
||||
// This apparantly happens
|
||||
}
|
||||
}
|
||||
if (previous) {
|
||||
previous.host.after(oldChild.element);
|
||||
} else {
|
||||
//item.parent.prepend(oldChild.element);
|
||||
item.host.prepend(oldChild.element);
|
||||
// Out of order, detach
|
||||
if (matchedNode.attached) {
|
||||
item.host.removeChild(matchedNode.node);
|
||||
matchedNode.attached = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!oldChild) oldChild = oldChildren && oldChildren.splice(0, 1)[ 0 ];
|
||||
|
||||
child.previous = previous;
|
||||
if (oldChild && child.meta.normedType === oldChild.meta.normedType && childKey === oldChild.vnode.props?.key
|
||||
&& (child.meta.normedType !== Node || child.item.vnode === oldChild.vnode)) {
|
||||
// Update old-child
|
||||
child.item.host = oldChild.element;
|
||||
child.item.old = oldChild.vnode;
|
||||
queuedItems.push(child);
|
||||
} else {
|
||||
// New child
|
||||
if (oldChild) {
|
||||
if(oldChild.element) {
|
||||
if (oldChild.meta.renderer.remove){
|
||||
oldChild.meta.renderer.remove({
|
||||
...item,
|
||||
parent: item.host,
|
||||
host: oldChild.element
|
||||
});
|
||||
}else {
|
||||
item.host.removeChild(oldChild.element);
|
||||
} else if (node) {
|
||||
// No match by key
|
||||
// If old VNode associated, check if updatable
|
||||
if (node.old
|
||||
&& node.old.vnode?.props?.key === undefined
|
||||
&& node.old.meta.normedType === vItem.meta.normedType
|
||||
) {
|
||||
oldItem = node.old;
|
||||
matchedNode = node;
|
||||
++nodeI;
|
||||
} else {
|
||||
// Not updatable, detach
|
||||
if (node.attached) {
|
||||
item.host.removeChild(node.node);
|
||||
node.attached = false;
|
||||
}
|
||||
}else{
|
||||
// This apparantly happens
|
||||
}
|
||||
}
|
||||
queuedItems.push(child);
|
||||
}
|
||||
if (!child.meta.renderer.remove) {
|
||||
// If child is a node-type item track it as the previous (so we can insert next node-type items after it as intended)
|
||||
previous = child.item;
|
||||
}
|
||||
}
|
||||
while (oldChildren && oldChildren.length) {
|
||||
let oldChild = oldChildren.splice(0, 1)[ 0 ];
|
||||
if(oldChild.element) {
|
||||
if (oldChild.meta.renderer.remove) {
|
||||
oldChild.meta.renderer.remove({ ...item, parent: item.host, host: oldChild.element });
|
||||
} else {
|
||||
item.host.removeChild(oldChild.element);
|
||||
}
|
||||
}else{
|
||||
// This apparantly happens
|
||||
// If this is a special type and it was not matched to an old VNode by key, find it the first non-keyed
|
||||
// old VNode with the same normedType (These are ShadowDOM or Host-types)
|
||||
if(!oldItem){
|
||||
let indexOf = specialOldVChildren.findIndex(old=>old.meta.normedType===vItem.meta.normedType);
|
||||
if(indexOf>=0){
|
||||
oldItem = specialOldVChildren[indexOf];
|
||||
specialOldVChildren.splice(indexOf,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track that the node is used by a new node (we don't use this anywhere?!)
|
||||
if(matchedNode){
|
||||
matchedNode.new = vItem;
|
||||
}
|
||||
|
||||
/** @type {VRenderQueueItem} */
|
||||
let queueItem = {
|
||||
item: {
|
||||
vnode: vItem.vnode,
|
||||
document: item.document,
|
||||
host: matchedNode?.node,
|
||||
old: oldItem?.vnode,
|
||||
inSvg: item.inSvg,
|
||||
parent: item,
|
||||
},
|
||||
attached: matchedNode?.attached,
|
||||
previousNode: previousNodeItem,
|
||||
meta: vItem.meta
|
||||
}
|
||||
queueChildren.push(queueItem);
|
||||
if(vItem.meta.isNode){
|
||||
previousNodeItem = queueItem.item;
|
||||
}
|
||||
|
||||
++vItemI;
|
||||
}else if(node){
|
||||
// Trailing node item
|
||||
if(node.attached && !node.new) {
|
||||
// If this node was not reused for a new vnode representation, and was not already detached
|
||||
// for other reasons, remove it
|
||||
item.host.removeChild(node.node);
|
||||
node.attached = false;
|
||||
}
|
||||
++nodeI;
|
||||
}
|
||||
}
|
||||
|
||||
state.queue.splice(0, 0, ...queuedItems);
|
||||
state.queue.unshift(...queueChildren);
|
||||
|
||||
// Remove any special types (Host, ShadowDOM that were no longer present)
|
||||
for(let oldSpecialVnode of specialOldVChildren){
|
||||
oldSpecialVnode.meta.renderer.remove?.(oldSpecialVnode.vnode);
|
||||
}
|
||||
}
|
||||
|
||||
if (newlyCreated) {
|
||||
if (!meta.renderer.remove) {
|
||||
if (item.parent) {
|
||||
if (!previous) {
|
||||
// First child
|
||||
item.parent.prepend(item.host);
|
||||
} else {
|
||||
// Subsequent child
|
||||
previous.host.after(item.host);
|
||||
}
|
||||
}
|
||||
if (!attached && meta.isNode) {
|
||||
if (previousNode) {
|
||||
// Subsequent child
|
||||
previousNode.host.after(item.host);
|
||||
} else if (item.parent) {
|
||||
// First child
|
||||
item.parent.host.prepend(item.host);
|
||||
}
|
||||
if (!item.parent) newRoot = item.host;
|
||||
}
|
||||
if(!item.parent && !newRoot){
|
||||
newRoot = item.host;
|
||||
}
|
||||
}
|
||||
|
||||
@ -266,3 +235,46 @@ export function render(vnode, opts = {}) {
|
||||
}
|
||||
return newRoot;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Intermediate type used to process child-items
|
||||
* @typedef {object} FlattenedVNode
|
||||
* @property {VNode} vnode
|
||||
* @property {VNodeMeta} meta
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} NodeState
|
||||
* @property {ChildNode} node
|
||||
* @property {boolean} attached
|
||||
* @property {FlattenedOldVNode} old
|
||||
* @property {FlattenedVNode} new
|
||||
*/
|
||||
/**
|
||||
* Intermediate type used to process old child-items
|
||||
* @typedef {FlattenedVNode} FlattenedOldVNode
|
||||
* @property {NodeState} node - The DOM-node
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {VNode} vnode
|
||||
* @returns {FlattenedVNode[]}
|
||||
*/
|
||||
function flattenVNodeChildren(vnode){
|
||||
let children = [];
|
||||
let queue = (vnode?.children || []).slice();
|
||||
while (queue.length > 0) {
|
||||
let next = queue.shift();
|
||||
if (next instanceof Array) queue.splice(0, 0, ...next);
|
||||
else {
|
||||
let meta = getNodeMeta(next);
|
||||
if (meta) {
|
||||
children.push({ vnode: next, meta });
|
||||
}else{
|
||||
// null or undefined child, ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
@ -10,12 +10,22 @@ export const Host = Symbol('Host');
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const HostNodeRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: true,
|
||||
isNode: true, // Host node should always
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
create(item){
|
||||
if(!item.parent) throw new Error("Host node cannot appear as a top-level element unless a parent is provided");
|
||||
else return item.parent;
|
||||
if(!item.parent.host) throw new Error("Host node cannot appear as a top-level element unless a parent is provided");
|
||||
else return item.parent.host;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -31,7 +41,7 @@ export const HostNodeRenderer = {
|
||||
* @param {VRenderState} state
|
||||
*/
|
||||
update(item, state){
|
||||
item.host = item.host || item.parent;
|
||||
item.host = item.host || item.parent.host;
|
||||
NodeTreeRenderer.update(item,state);
|
||||
},
|
||||
};
|
||||
@ -7,6 +7,16 @@ import '../types';
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const NativeRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: false,// Counter intiutitive, but this is false as CSX will never manage children of a premade DOM-element
|
||||
isNode: true,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
@ -18,6 +28,9 @@ export const NativeRenderer = {
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
update(item){
|
||||
return;// NO-OP
|
||||
if(item.old && item.old!==item.vnode){
|
||||
item.host.replaceWith(item.vnode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@ -9,6 +9,16 @@ export const Primitive = Symbol("primitive");
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const PrimitiveRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: false,// Primitive type (TextNode) can't have children
|
||||
isNode: true,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
|
||||
@ -12,6 +12,16 @@ let namespace = {
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const NodeTreeRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: true,
|
||||
isNode: true,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {VRenderItem} item
|
||||
|
||||
@ -9,12 +9,22 @@ export const ShadowDOM = Symbol('ShadowDOM');
|
||||
* @implements {VNodeRenderer}
|
||||
*/
|
||||
export const ShadowNodeRenderer = {
|
||||
/**
|
||||
* @return {VNodeRendererMeta}
|
||||
*/
|
||||
meta(){
|
||||
return {
|
||||
hasChildren: true,
|
||||
isNode: false,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
create(item) {
|
||||
if (!item.parent) throw new Error("ShadowDOM node cannot appear as a top-level element unless a parent is provided");
|
||||
else return item.parent.shadowRoot || item.parent.attachShadow({ mode: 'open' });// TODO Pass props as options? (e.g. delegateFocus, mode)
|
||||
else return item.parent.host.shadowRoot || item.parent.host.attachShadow({ mode: 'open' });// TODO Pass props as options? (e.g. delegateFocus, mode)
|
||||
},
|
||||
|
||||
/**
|
||||
@ -28,6 +38,6 @@ export const ShadowNodeRenderer = {
|
||||
* @param {VRenderItem} item
|
||||
*/
|
||||
update(item) {
|
||||
item.host = item.host || item.parent.shadowRoot;
|
||||
item.host = item.host || item.parent.host.shadowRoot;
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,3 +2,4 @@ export * from "./vnode";
|
||||
export * from "./render-item";
|
||||
export * from "./render-state";
|
||||
export * from "./vnode-renderer";
|
||||
export * from "./renderer-meta";
|
||||
@ -2,13 +2,12 @@ import './vnode';
|
||||
|
||||
/**
|
||||
* Per node rendering-state when rendering a tree of VNodes
|
||||
* @typedef VRenderItem
|
||||
* @interface
|
||||
* @interface VRenderItem
|
||||
* @category VDOM.renderer
|
||||
* @property {VNode} vnode - The VNode representation to update to
|
||||
* @property {VNode} [old] - The previous VNode representation of this item
|
||||
* @property {Element} host - The DOM-node being rendered
|
||||
* @property {Document} document - The DOM-document to be added to
|
||||
* @property {boolean} inSvg - Indicates whether this node is a child of an SVG element, and should thus be created with createElementNS(...)
|
||||
* @property {Element} [parent] - Parent DOM-node
|
||||
* @property {VRenderItem} [parent] - Parent render item
|
||||
**/
|
||||
|
||||
@ -6,8 +6,7 @@ import "./vnode";
|
||||
|
||||
/**
|
||||
* Per node rendering-state when rendering a tree of VNodes
|
||||
* @typedef VRenderQueueItemMetadata
|
||||
* @interface
|
||||
* @typedef {object} VRenderQueueItemMetadata
|
||||
* @category VDOM.renderer
|
||||
* @property {VNodeRenderer} renderer - The renderer that will render this item
|
||||
* @property {VNodeType} normedType - The normed type of a VNode, for most VNode this just maps to vnode.type, but a text-node normally does not have a type.
|
||||
@ -16,30 +15,28 @@ import "./vnode";
|
||||
|
||||
/**
|
||||
* Per node rendering-state when rendering a tree of VNodes
|
||||
* @typedef VRenderQueueItem
|
||||
* @interface
|
||||
* @typedef {object} VRenderQueueItem
|
||||
* @category VDOM.renderer
|
||||
* @property {VRenderItem} item - The item to queue for rendering
|
||||
* @property {VRenderItem} item - The item queued for rendering
|
||||
* @property {VRenderQueueItemMetadata} meta - Meta data for the item such as normedType and the renderer to use(from a preprocessing stage)
|
||||
* @property {VRenderItem} previous - The item that will have been inserted before this one
|
||||
* @property {VRenderItem} [previousNode] - The previous node-item that will have been inserted before this one
|
||||
* @property {boolean} [attached] - Whether the host-node was already attached to the dom
|
||||
**/
|
||||
|
||||
/**
|
||||
* Temporary data structure for listing an old VNode
|
||||
* @typedef VOldQueueItem
|
||||
* @interface
|
||||
* @typedef {object} VOldQueueItem
|
||||
* @category VDOM.renderer
|
||||
* @property {VNode} vnode - The old vnode
|
||||
* @property {VRenderQueueItemMetadata} meta - Meta data for the item such as normedType and the renderer to use(from a preprocessing stage)
|
||||
* @property {Element} element - The matching element
|
||||
* @property {boolean} detached - Indicates if the element was already removed
|
||||
**/
|
||||
|
||||
/**
|
||||
* Global rendering-state when rendering a tree of VNodes
|
||||
* @typedef VRenderState
|
||||
* @interface
|
||||
* @typedef {object} VRenderState
|
||||
* @category VDOM.renderer
|
||||
* @property {Array.<VRenderQueueItem>} queue - The queue of items to be rendered
|
||||
* @property {Array.<[Function,Element]>} refs - Ref-callback functions be called when rendering is done
|
||||
* @property {Map.<string, VOldQueueItem>} keyedElements - A map of (old) keyed elements
|
||||
**/
|
||||
|
||||
9
packages/csx/src/vdom/types/renderer-meta.js
Normal file
9
packages/csx/src/vdom/types/renderer-meta.js
Normal file
@ -0,0 +1,9 @@
|
||||
import './vnode';
|
||||
|
||||
/**
|
||||
* Per node rendering-state when rendering a tree of VNodes
|
||||
* @interface VNodeRendererMeta
|
||||
* @category VDOM.renderer
|
||||
* @property {boolean} isNode - Whether the node-type handled by this renderer has a place in the DOM hierarchy
|
||||
* @property {boolean} hasChildren - Indicates if the node-type handled by this renderer can have child-nodes
|
||||
**/
|
||||
@ -1,18 +1,26 @@
|
||||
import "./render-item";// Info about what we're rendering and where to
|
||||
import "./renderer-meta"; // Meta-data about the type of node handled
|
||||
|
||||
// Note: This type is not meant to be public
|
||||
|
||||
/**
|
||||
* Represents a renderer capable of rendering a VNode of a certain type
|
||||
* @interface VNodeRenderer
|
||||
* @class
|
||||
**/
|
||||
|
||||
/**
|
||||
* This method creates the element corresponding to a vnode
|
||||
* @method
|
||||
* @name VNodeRenderer#meta
|
||||
* @returns {VNodeRendererMeta}
|
||||
*/
|
||||
|
||||
/**
|
||||
* This method creates the element corresponding to a vnode
|
||||
* @method
|
||||
* @name VNodeRenderer#create
|
||||
* @param {VRenderItem} item
|
||||
* @param {VRenderItem} parent
|
||||
* @returns {Element}
|
||||
*/
|
||||
|
||||
|
||||
@ -18,8 +18,7 @@
|
||||
|
||||
/**
|
||||
* A tree of virtual-nodes (e.g, type,props,attr and nested children)
|
||||
* @typedef VNodeTree
|
||||
* @interface
|
||||
* @interface VNodeTree
|
||||
* @category VDOM
|
||||
* @property {VNodeType} type - TagName or CustomElement of the html-element
|
||||
* @property {VNodeProps} props - Properties to set on the element
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user