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>
|
<li>
|
||||||
<a href="./table/">Tables (arrow functions)</a>
|
<a href="./table/">Tables (arrow functions)</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./table-2/">Tables 2 (Key-prop)</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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(''));
|
].join(''));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Nulls are ignored", async () => {
|
test("Nulls and undefined are ignored", async () => {
|
||||||
expect(
|
expect(
|
||||||
testContainer(
|
testContainer(
|
||||||
render(
|
render(
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Title</h1>
|
<h1>Title</h1>
|
||||||
{null}
|
{null}
|
||||||
|
{undefined}
|
||||||
<button>Save</button>
|
<button>Save</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -212,4 +213,50 @@ describe("Children", () => {
|
|||||||
`</div>`
|
`</div>`
|
||||||
].join(''));
|
].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", () => {
|
describe("Key-property tests", () => {
|
||||||
test("Keyed list", async () => {
|
test("Keyed list", async () => {
|
||||||
let renderedIndexes = [];
|
let initialRendered = new Map();
|
||||||
let initIndexes = [0, 1, 2, 3];
|
let initIndexes = [1, 2, 3, 4];
|
||||||
|
|
||||||
let makeSpec = (targetList, indexes)=>(
|
let makeSpec = (targetList, indexes)=>(
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
indexes.map(index => (
|
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}
|
{index}
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
@ -18,7 +18,7 @@ describe("Key-property tests", () => {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|
||||||
let initialVSpec = makeSpec(renderedIndexes, initIndexes);
|
let initialVSpec = makeSpec(initialRendered, initIndexes);
|
||||||
|
|
||||||
let rendered = render(initialVSpec);
|
let rendered = render(initialVSpec);
|
||||||
let container = testContainer(rendered);
|
let container = testContainer(rendered);
|
||||||
@ -31,17 +31,18 @@ describe("Key-property tests", () => {
|
|||||||
].join('')
|
].join('')
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(renderedIndexes.length).toBe(4);
|
expect(initialRendered.size).toBe(4);
|
||||||
for(let rendered of renderedIndexes){
|
for(let rendered of initialRendered){
|
||||||
expect(rendered).not.toBeUndefined();
|
expect(rendered).not.toBeUndefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
let reorderedIndexes = [3,2,1,0];
|
// Reverse order
|
||||||
let rerenderedIndexes = renderedIndexes.slice();
|
let reorderedIndexes = [4,3,2,1];
|
||||||
|
let rerenderedIndexes = new Map(initialRendered);
|
||||||
let updatedVSpec = makeSpec(rerenderedIndexes, reorderedIndexes);
|
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(
|
expect(container.innerHTML).toBe(
|
||||||
[
|
[
|
||||||
`<ul>`,
|
`<ul>`,
|
||||||
@ -51,11 +52,46 @@ describe("Key-property tests", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Validate that items were merely re-arranged and not re-created
|
// Validate that items were merely re-arranged and not re-created
|
||||||
expect(rerenderedIndexes.length).toBe(4);
|
expect(rerenderedIndexes.size).toBe(4);
|
||||||
for(let i=0; i<4; ++i){
|
for(let i of initIndexes){
|
||||||
let initRendered = renderedIndexes[i];
|
let initRendered = initialRendered.get(i);
|
||||||
let reorderedRendered = rerenderedIndexes[i];
|
let reorderedRendered = rerenderedIndexes.get(i);
|
||||||
expect(initRendered === reorderedRendered).toBe(true); // These should've remained the same
|
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/
|
# 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/*
|
node_modules/*
|
||||||
rollup.config.js
|
rollup.config.js
|
||||||
yarn.lock
|
yarn.lock
|
||||||
@ -110,7 +110,7 @@ export class CsxConfig {
|
|||||||
let srcOpts = {
|
let srcOpts = {
|
||||||
es: format==='es',
|
es: format==='es',
|
||||||
single: type==='lib',
|
single: type==='lib',
|
||||||
sourcemap: true,// Just always there for now
|
sourcemap: minified ? true : (format==='es'? false : true),
|
||||||
minified: !!minified
|
minified: !!minified
|
||||||
};
|
};
|
||||||
let outDir = type==='lib'? lib : dist;
|
let outDir = type==='lib'? lib : dist;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cerxes/csx",
|
"name": "@cerxes/csx",
|
||||||
"version": "0.0.11",
|
"version": "0.0.13",
|
||||||
"author": "Miel Truyen <miel.truyen@cerxes.net>",
|
"author": "Miel Truyen <miel.truyen@cerxes.net>",
|
||||||
"description": "CSX is a minimalistic UI-framework inspired by React+JSX for usage with WebComponents.",
|
"description": "CSX is a minimalistic UI-framework inspired by React+JSX for usage with WebComponents.",
|
||||||
"repository": {
|
"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 './types';
|
||||||
import {
|
import { getNodeMeta } from "./node-meta";
|
||||||
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?)
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This exists as a very basic example/test for JSX-to-DOM
|
* This exists as a very basic example/test for JSX-to-DOM
|
||||||
* @category VDOM
|
* @category VDOM
|
||||||
* @param {VNode} vnode
|
* @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}
|
* @return {Element}
|
||||||
*/
|
*/
|
||||||
export function render(vnode, opts = {}) {
|
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)
|
// 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
|
// 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)
|
// 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)
|
// or are some other special type of node (like Host or ShadowDOM)
|
||||||
/**
|
/** @type {VRenderState} */
|
||||||
*
|
|
||||||
* @type {VRenderState}
|
|
||||||
*/
|
|
||||||
let state = {
|
let state = {
|
||||||
keyedElements: new Map(),
|
|
||||||
refs: [],
|
refs: [],
|
||||||
queue: [{
|
queue: [{
|
||||||
// Start item
|
// Start item
|
||||||
item: {
|
item: {
|
||||||
document: opts.document || document,
|
document: opts.document || document,
|
||||||
host: opts.host,
|
host: opts.host,
|
||||||
parent: opts.parent,
|
|
||||||
old: opts.old,
|
old: opts.old,
|
||||||
vnode: vnode
|
vnode: vnode
|
||||||
},
|
},
|
||||||
meta: getNodeMeta(vnode)
|
meta: getNodeMeta(vnode),
|
||||||
|
parent: null,
|
||||||
|
attached: true// Root item is treated as if it is attached
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
let newRoot = undefined;
|
let newRoot = undefined;
|
||||||
while (state.queue.length > 0) {
|
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;
|
let renderer = meta.renderer;
|
||||||
if (!renderer) throw new Error("No renderer for vnode", item.vnode);
|
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;
|
if (!item.inSvg && item.vnode?.type === 'svg') item.inSvg = true;
|
||||||
else if (item.inSvg && item.vnode?.type === 'foreignObject') item.inSvg = false;
|
else if (item.inSvg && item.vnode?.type === 'foreignObject') item.inSvg = false;
|
||||||
|
|
||||||
@ -75,7 +57,8 @@ export function render(vnode, opts = {}) {
|
|||||||
item.host = renderer.create(item, meta);
|
item.host = renderer.create(item, meta);
|
||||||
newlyCreated = true;
|
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]);
|
state.refs.push([item.vnode.props.ref, item.host]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,180 +67,166 @@ export function render(vnode, opts = {}) {
|
|||||||
renderer.update(item, meta);
|
renderer.update(item, meta);
|
||||||
|
|
||||||
// Update children
|
// Update children
|
||||||
if (meta.normedType !== Node && (item.vnode?.children || item.old?.children)) {
|
if (meta.hasChildren) {
|
||||||
let childTypes = new Set();
|
/** @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)
|
// Iterate old children and link them to their corresponding elements
|
||||||
/**
|
let nodeI = 0, nodesLength = nodes.length;
|
||||||
* @type { Object.<VNodeType, Array.<VRenderQueueItem>> }
|
let oldVItemI = 0, oldVItemLength = oldVChildren.length;
|
||||||
*/
|
while(oldVItemI < oldVItemLength){
|
||||||
let vChildren = {};
|
/** @type {FlattenedOldVNode} */
|
||||||
let queue = (item.vnode?.children || []).slice();
|
let oldVItem = oldVChildren[oldVItemI];
|
||||||
while (queue.length > 0) {
|
/** @type {NodeState} */
|
||||||
let next = queue.splice(0, 1)[ 0 ];
|
let node = nodes[nodeI];
|
||||||
if (next instanceof Array) queue.splice(0, 0, ...next);
|
let nodeKey = oldVItem.vnode?.props?.key
|
||||||
else {
|
if(nodeKey !== undefined){
|
||||||
let meta = getNodeMeta(next);
|
keyedChildren.set(nodeKey, oldVItem);
|
||||||
if (meta && meta.renderer) {
|
}
|
||||||
// Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
|
++oldVItemI;
|
||||||
let childType = meta.normedType;
|
if(oldVItem.meta.isNode && node){
|
||||||
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)
|
oldVItem.node = node;
|
||||||
childTypes.add(childType);// Track that children of this type exist and should be iterated later
|
node.old = oldVItem;
|
||||||
vChildren[ childType ] = vChildren[ childType ] || []; // Make sure the array exists
|
++nodeI;
|
||||||
vChildren[ childType ].push({
|
}else if(nodeKey === undefined){
|
||||||
item: {
|
specialOldVChildren.push(oldVItem);
|
||||||
...item,
|
|
||||||
old: undefined,
|
|
||||||
vnode: next,
|
|
||||||
host: undefined,
|
|
||||||
parent: item.host
|
|
||||||
},
|
|
||||||
meta: meta
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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
|
// Iterate new children, remove old nodes as needed and create new queue items
|
||||||
/**
|
/** @type {VRenderQueueItem[]} */
|
||||||
* @type { Object.<VNodeType, Array.<VOldQueueItem>> }
|
let queueChildren = [];
|
||||||
*/
|
/** @type {VRenderItem} */
|
||||||
let oldVChildren = {};
|
let previousNodeItem = undefined;
|
||||||
let curElement = item.host.firstChild;
|
let vItemI = 0, vItemLength = vChildren.length;
|
||||||
queue = (item.old?.children || []).slice();
|
nodeI = 0;
|
||||||
while (queue.length > 0) {
|
while(vItemI < vItemLength || nodeI < nodesLength){
|
||||||
let next = queue.splice(0, 1)[ 0 ];
|
let vItem = vChildren[vItemI];
|
||||||
if (next instanceof Array) queue.splice(0, 0, ...next);
|
let node = nodes[nodeI];
|
||||||
else {
|
|
||||||
let meta = getNodeMeta(next);
|
if(vItem){
|
||||||
if (meta && meta.renderer) {
|
let nodeKey = vItem?.vnode?.props?.key;
|
||||||
// Only items with a renderer are tracked (any other are undefined or null and shoulnd't be rendered at all)
|
/** @type {FlattenedOldVNode} */
|
||||||
let childType = meta.normedType;
|
let oldItem = undefined;
|
||||||
let childElement;
|
if(nodeKey!==undefined){
|
||||||
if (!meta.renderer.remove) {
|
oldItem = keyedChildren.get(nodeKey);
|
||||||
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
|
/** @type {NodeState} */
|
||||||
let queuedItems = [];
|
let matchedNode = undefined;
|
||||||
/**@type {VRenderQueueItem}*/ let previous = null;
|
|
||||||
for (let childType of sortedChildTypes) {
|
|
||||||
let newChildren = vChildren[ childType ];
|
|
||||||
let oldChildren = oldVChildren[ childType ];
|
|
||||||
|
|
||||||
while (newChildren && newChildren.length) {
|
// If this is a DOM-node-type match it to an existing node
|
||||||
let child = newChildren.splice(0, 1)[ 0 ];
|
if(vItem.meta.isNode) {
|
||||||
|
// Match new item to existing node when possible
|
||||||
// Key handling
|
matchedNode = oldItem?.node;
|
||||||
let childKey = child.item.vnode.props?.key;
|
if (matchedNode) {
|
||||||
/**@type {VOldQueueItem}*/ let oldChild;
|
// DOM-node matched by key
|
||||||
if (childKey) {
|
if (matchedNode === node) {
|
||||||
oldChild = state.keyedElements.get(childKey);
|
// Item in order with old representation
|
||||||
if (oldChild) {
|
++nodeI;
|
||||||
if (oldChildren && oldChildren[ 0 ] === oldChild) {
|
|
||||||
// Old keyed child already in the right place (just clear it from the queue);
|
|
||||||
oldChildren.splice(0, 1);
|
|
||||||
} else {
|
} else {
|
||||||
// Old keyed child not already in the right place
|
// Out of order, detach
|
||||||
let indexOfKeyed = oldChildren.indexOf(oldChild);
|
if (matchedNode.attached) {
|
||||||
if (indexOfKeyed) {
|
item.host.removeChild(matchedNode.node);
|
||||||
oldChildren.splice(indexOfKeyed, 1);
|
matchedNode.attached = false;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else if (node) {
|
||||||
}
|
// No match by key
|
||||||
if (!oldChild) oldChild = oldChildren && oldChildren.splice(0, 1)[ 0 ];
|
// If old VNode associated, check if updatable
|
||||||
|
if (node.old
|
||||||
child.previous = previous;
|
&& node.old.vnode?.props?.key === undefined
|
||||||
if (oldChild && child.meta.normedType === oldChild.meta.normedType && childKey === oldChild.vnode.props?.key
|
&& node.old.meta.normedType === vItem.meta.normedType
|
||||||
&& (child.meta.normedType !== Node || child.item.vnode === oldChild.vnode)) {
|
) {
|
||||||
// Update old-child
|
oldItem = node.old;
|
||||||
child.item.host = oldChild.element;
|
matchedNode = node;
|
||||||
child.item.old = oldChild.vnode;
|
++nodeI;
|
||||||
queuedItems.push(child);
|
} else {
|
||||||
} else {
|
// Not updatable, detach
|
||||||
// New child
|
if (node.attached) {
|
||||||
if (oldChild) {
|
item.host.removeChild(node.node);
|
||||||
if(oldChild.element) {
|
node.attached = false;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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{
|
}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 (!attached && meta.isNode) {
|
||||||
if (!meta.renderer.remove) {
|
if (previousNode) {
|
||||||
if (item.parent) {
|
// Subsequent child
|
||||||
if (!previous) {
|
previousNode.host.after(item.host);
|
||||||
// First child
|
} else if (item.parent) {
|
||||||
item.parent.prepend(item.host);
|
// First child
|
||||||
} else {
|
item.parent.host.prepend(item.host);
|
||||||
// Subsequent child
|
|
||||||
previous.host.after(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;
|
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}
|
* @implements {VNodeRenderer}
|
||||||
*/
|
*/
|
||||||
export const HostNodeRenderer = {
|
export const HostNodeRenderer = {
|
||||||
|
/**
|
||||||
|
* @return {VNodeRendererMeta}
|
||||||
|
*/
|
||||||
|
meta(){
|
||||||
|
return {
|
||||||
|
hasChildren: true,
|
||||||
|
isNode: true, // Host node should always
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {VRenderItem} item
|
* @param {VRenderItem} item
|
||||||
*/
|
*/
|
||||||
create(item){
|
create(item){
|
||||||
if(!item.parent) throw new Error("Host node cannot appear as a top-level element unless a parent is provided");
|
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;
|
else return item.parent.host;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,7 +41,7 @@ export const HostNodeRenderer = {
|
|||||||
* @param {VRenderState} state
|
* @param {VRenderState} state
|
||||||
*/
|
*/
|
||||||
update(item, state){
|
update(item, state){
|
||||||
item.host = item.host || item.parent;
|
item.host = item.host || item.parent.host;
|
||||||
NodeTreeRenderer.update(item,state);
|
NodeTreeRenderer.update(item,state);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -7,6 +7,16 @@ import '../types';
|
|||||||
* @implements {VNodeRenderer}
|
* @implements {VNodeRenderer}
|
||||||
*/
|
*/
|
||||||
export const NativeRenderer = {
|
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
|
* @param {VRenderItem} item
|
||||||
*/
|
*/
|
||||||
@ -18,6 +28,9 @@ export const NativeRenderer = {
|
|||||||
* @param {VRenderItem} item
|
* @param {VRenderItem} item
|
||||||
*/
|
*/
|
||||||
update(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}
|
* @implements {VNodeRenderer}
|
||||||
*/
|
*/
|
||||||
export const PrimitiveRenderer = {
|
export const PrimitiveRenderer = {
|
||||||
|
/**
|
||||||
|
* @return {VNodeRendererMeta}
|
||||||
|
*/
|
||||||
|
meta(){
|
||||||
|
return {
|
||||||
|
hasChildren: false,// Primitive type (TextNode) can't have children
|
||||||
|
isNode: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {VRenderItem} item
|
* @param {VRenderItem} item
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -12,6 +12,16 @@ let namespace = {
|
|||||||
* @implements {VNodeRenderer}
|
* @implements {VNodeRenderer}
|
||||||
*/
|
*/
|
||||||
export const NodeTreeRenderer = {
|
export const NodeTreeRenderer = {
|
||||||
|
/**
|
||||||
|
* @return {VNodeRendererMeta}
|
||||||
|
*/
|
||||||
|
meta(){
|
||||||
|
return {
|
||||||
|
hasChildren: true,
|
||||||
|
isNode: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {VRenderItem} item
|
* @param {VRenderItem} item
|
||||||
|
|||||||
@ -9,12 +9,22 @@ export const ShadowDOM = Symbol('ShadowDOM');
|
|||||||
* @implements {VNodeRenderer}
|
* @implements {VNodeRenderer}
|
||||||
*/
|
*/
|
||||||
export const ShadowNodeRenderer = {
|
export const ShadowNodeRenderer = {
|
||||||
|
/**
|
||||||
|
* @return {VNodeRendererMeta}
|
||||||
|
*/
|
||||||
|
meta(){
|
||||||
|
return {
|
||||||
|
hasChildren: true,
|
||||||
|
isNode: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {VRenderItem} item
|
* @param {VRenderItem} item
|
||||||
*/
|
*/
|
||||||
create(item) {
|
create(item) {
|
||||||
if (!item.parent) throw new Error("ShadowDOM node cannot appear as a top-level element unless a parent is provided");
|
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
|
* @param {VRenderItem} item
|
||||||
*/
|
*/
|
||||||
update(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-item";
|
||||||
export * from "./render-state";
|
export * from "./render-state";
|
||||||
export * from "./vnode-renderer";
|
export * from "./vnode-renderer";
|
||||||
|
export * from "./renderer-meta";
|
||||||
@ -2,13 +2,12 @@ import './vnode';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Per node rendering-state when rendering a tree of VNodes
|
* Per node rendering-state when rendering a tree of VNodes
|
||||||
* @typedef VRenderItem
|
* @interface VRenderItem
|
||||||
* @interface
|
|
||||||
* @category VDOM.renderer
|
* @category VDOM.renderer
|
||||||
* @property {VNode} vnode - The VNode representation to update to
|
* @property {VNode} vnode - The VNode representation to update to
|
||||||
* @property {VNode} [old] - The previous VNode representation of this item
|
* @property {VNode} [old] - The previous VNode representation of this item
|
||||||
* @property {Element} host - The DOM-node being rendered
|
* @property {Element} host - The DOM-node being rendered
|
||||||
* @property {Document} document - The DOM-document to be added to
|
* @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 {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
|
* Per node rendering-state when rendering a tree of VNodes
|
||||||
* @typedef VRenderQueueItemMetadata
|
* @typedef {object} VRenderQueueItemMetadata
|
||||||
* @interface
|
|
||||||
* @category VDOM.renderer
|
* @category VDOM.renderer
|
||||||
* @property {VNodeRenderer} renderer - The renderer that will render this item
|
* @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.
|
* @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
|
* Per node rendering-state when rendering a tree of VNodes
|
||||||
* @typedef VRenderQueueItem
|
* @typedef {object} VRenderQueueItem
|
||||||
* @interface
|
|
||||||
* @category VDOM.renderer
|
* @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 {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
|
* Temporary data structure for listing an old VNode
|
||||||
* @typedef VOldQueueItem
|
* @typedef {object} VOldQueueItem
|
||||||
* @interface
|
|
||||||
* @category VDOM.renderer
|
* @category VDOM.renderer
|
||||||
* @property {VNode} vnode - The old vnode
|
* @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 {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 {Element} element - The matching element
|
||||||
|
* @property {boolean} detached - Indicates if the element was already removed
|
||||||
**/
|
**/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global rendering-state when rendering a tree of VNodes
|
* Global rendering-state when rendering a tree of VNodes
|
||||||
* @typedef VRenderState
|
* @typedef {object} VRenderState
|
||||||
* @interface
|
|
||||||
* @category VDOM.renderer
|
* @category VDOM.renderer
|
||||||
* @property {Array.<VRenderQueueItem>} queue - The queue of items to be rendered
|
* @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 {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 "./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
|
// Note: This type is not meant to be public
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a renderer capable of rendering a VNode of a certain type
|
* Represents a renderer capable of rendering a VNode of a certain type
|
||||||
* @interface VNodeRenderer
|
* @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
|
* This method creates the element corresponding to a vnode
|
||||||
* @method
|
* @method
|
||||||
* @name VNodeRenderer#create
|
* @name VNodeRenderer#create
|
||||||
* @param {VRenderItem} item
|
* @param {VRenderItem} item
|
||||||
|
* @param {VRenderItem} parent
|
||||||
* @returns {Element}
|
* @returns {Element}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,7 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A tree of virtual-nodes (e.g, type,props,attr and nested children)
|
* A tree of virtual-nodes (e.g, type,props,attr and nested children)
|
||||||
* @typedef VNodeTree
|
* @interface VNodeTree
|
||||||
* @interface
|
|
||||||
* @category VDOM
|
* @category VDOM
|
||||||
* @property {VNodeType} type - TagName or CustomElement of the html-element
|
* @property {VNodeType} type - TagName or CustomElement of the html-element
|
||||||
* @property {VNodeProps} props - Properties to set on the element
|
* @property {VNodeProps} props - Properties to set on the element
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user