Initial rendering of a Todos-MVC app. (events are called but don't trigger a re-rendering yet, renderin-function does not yet support updating DOM yet)
This commit is contained in:
parent
31cfda50f5
commit
5169c5018d
@ -1,8 +1,22 @@
|
|||||||
|
import {render} from "../vdom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This CustomElement class is to avoid having to do an ugly workaround in every custom-element:
|
* This CustomElement class is to avoid having to do an ugly workaround in every custom-element:
|
||||||
* Which would be replacing 'HTMLElement' with '(class extends HTMLElement{})'
|
* Which would be replacing 'HTMLElement' with '(class extends HTMLElement{})'
|
||||||
*
|
*
|
||||||
* Also, it is a good starting point for implementing render() functionality, listening to props, state changes, events and whatnot (use decorators)
|
* Also, it is a good starting point for implementing render() functionality, listening to props, state changes, events and whatnot (use decorators)
|
||||||
*/
|
*/
|
||||||
export class CustomElement extends HTMLElement {}
|
export class CustomElement extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
if(this.render){
|
||||||
|
let newVNode = this.render();
|
||||||
|
render(newVNode, {
|
||||||
|
host: this
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disconnectedCallback(){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,3 +16,6 @@ export const VNODEPROP_EXCLUDE_DIRECT = {
|
|||||||
export const VNODEPROP_IGNORE = {
|
export const VNODEPROP_IGNORE = {
|
||||||
['key']: true,
|
['key']: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Host = Symbol('host');
|
||||||
|
export const ShadowDOM = Symbol('shadow-dom');
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./vnode";
|
export * from "./vnode";
|
||||||
export * from "./render";
|
export * from "./render";
|
||||||
|
export {Host, ShadowDOM} from "./constants";
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import './vnode';
|
import './vnode';
|
||||||
import {
|
import {
|
||||||
|
Host, ShadowDOM,
|
||||||
VNODE_EXCLUDE,
|
VNODE_EXCLUDE,
|
||||||
VNODEPROP_DIRECT, VNODEPROP_EXCLUDE_DIRECT,
|
VNODEPROP_DIRECT, VNODEPROP_EXCLUDE_DIRECT,
|
||||||
VNODEPROP_IGNORE,
|
VNODEPROP_IGNORE,
|
||||||
@ -21,7 +22,11 @@ import {
|
|||||||
* @return {Element}
|
* @return {Element}
|
||||||
*/
|
*/
|
||||||
export function render(vnode, opts = {}) {
|
export function render(vnode, opts = {}) {
|
||||||
// Replace how this works into a queue instead of a recursive call, also consider changing JSX to support (changed)="..." notation
|
// TODO figure out how to handle events (its not that easy to create (click)={this.onClick} or something, that is not supported by the @babel/parser and we'd have to fork it..
|
||||||
|
// ---> We've got a basic onClick (react-style) system set up now
|
||||||
|
// TODO innerHTML, innerText and other tags/props that are trickyer then just mapping value to attribute
|
||||||
|
// TODO: Replace how this works into a queue instead of a recursive call, also consider changing JSX to support (changed)="..." notation
|
||||||
|
// TODO ref-prop (should it only return once all child els are created and appended to the child?!)
|
||||||
let {
|
let {
|
||||||
/**
|
/**
|
||||||
* @type {Element}
|
* @type {Element}
|
||||||
@ -30,61 +35,84 @@ export function render(vnode, opts = {}) {
|
|||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
if(VNODE_EXCLUDE[vnode]) return undefined;
|
if(VNODE_EXCLUDE[vnode]) return undefined;
|
||||||
console.log(vnode);
|
|
||||||
|
|
||||||
if(vnode instanceof Object){
|
if(!host){
|
||||||
// Type
|
if(!['object', 'function', 'symbol'].includes(typeof(vnode))){
|
||||||
let tagName = vnode.type instanceof Object? vnode.type.tagName : vnode.type;
|
host = document.createTextNode(vnode);
|
||||||
if(!host) host = document.createElement(tagName);
|
}else if(typeof(vnode?.type) === 'string'){
|
||||||
|
host = document.createElement(vnode.type);
|
||||||
|
}else if(vnode?.type?.tagName){
|
||||||
|
host = document.createElement(vnode.type.tagName);
|
||||||
|
}else{
|
||||||
|
throw new Error("Unrecognized vnode type", vnode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
if (vnode.props) {
|
if (vnode?.props) {
|
||||||
if (vnode.props.style && typeof (vnode.props.style) === 'object') {
|
if (vnode.props.style && typeof (vnode.props.style) === 'object') {
|
||||||
for (let styleKey in vnode.props.style) {
|
for (let styleKey in vnode.props.style) {
|
||||||
host.style[ styleKey ] = vnode.props.style[ styleKey ];
|
host.style[ styleKey ] = vnode.props.style[ styleKey ];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for (let key in vnode.props) {
|
}
|
||||||
let val = vnode.props[key];
|
for (let key in vnode.props) {
|
||||||
if(VNODEPROP_IGNORE[key]){
|
let val = vnode.props[key];
|
||||||
// NO-OP
|
if(VNODEPROP_IGNORE[key]){
|
||||||
}else if(VNODEPROP_DIRECT[key]){
|
// NO-OP
|
||||||
|
}else if(VNODEPROP_DIRECT[key]){
|
||||||
|
host[key] = val;
|
||||||
|
}else{
|
||||||
|
if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){
|
||||||
host[key] = val;
|
host[key] = val;
|
||||||
}else{
|
}
|
||||||
if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){
|
if(key.slice(0,2)==='on' && key[2]>='A' && key[2]<='Z'){
|
||||||
host[key] = val;
|
if(val instanceof Function){
|
||||||
|
host.addEventListener(
|
||||||
|
// Convert camelCase to dash-case
|
||||||
|
key[2].toLowerCase()+key.slice(3).replace(/[A-Z]/g, function(c){return('-'+c.toLowerCase())}),
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}else{
|
||||||
|
new Error("Unsupported event-handler");
|
||||||
}
|
}
|
||||||
if (val === false) {
|
|
||||||
|
}else {
|
||||||
|
if (val === false || val===null || val==='') {
|
||||||
host.removeAttribute(key);
|
host.removeAttribute(key);
|
||||||
} else if (val === true) {
|
} else if (val === true) {
|
||||||
host.setAttribute(key, "");
|
host.setAttribute(key, "");
|
||||||
} else{
|
} else {
|
||||||
host.setAttribute(key, val);
|
host.setAttribute(key, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Children
|
// Children
|
||||||
if (vnode.children) {
|
if (vnode?.children) {
|
||||||
let children = vnode.children instanceof Array? vnode.children : [vnode.children];
|
let queue = vnode.children instanceof Array? vnode.children.slice() : [vnode.children];
|
||||||
|
while(queue.length){
|
||||||
for(let child of children){
|
let child = queue.splice(0,1)[0];
|
||||||
let el = child instanceof Element? child : render(child, {
|
if(child instanceof Array){
|
||||||
...opts,
|
queue.splice(0,0,...child);
|
||||||
host: undefined
|
}else{
|
||||||
});
|
if(child?.type === ShadowDOM){
|
||||||
if(el!==undefined){
|
let shadow = host.attachShadow({mode: 'open'});
|
||||||
host.appendChild(el);
|
render({children: child.children}, {
|
||||||
|
...opts,
|
||||||
|
host: shadow
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
let el = child instanceof Element? child : render(child, {
|
||||||
|
...opts,
|
||||||
|
host: undefined
|
||||||
|
});
|
||||||
|
if(el!==undefined) host.appendChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO figure out how to handle events (its not that easy to create (click)={this.onClick} or something, that is not supporter by the @babel/parser and we'd have to fork it..
|
|
||||||
// TODO ref-prop (should it only return once all child els are created and appended to the child?!)
|
|
||||||
// TODO innerHTML, innerText and other tags/props that are trickyer then just mapping value to attribute
|
|
||||||
}else{
|
|
||||||
if(!host) host = document.createTextNode(vnode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return host;
|
return host;
|
||||||
|
|||||||
@ -21,7 +21,9 @@ export default [
|
|||||||
plugins: [
|
plugins: [
|
||||||
sass(),
|
sass(),
|
||||||
babel(), // babel
|
babel(), // babel
|
||||||
resolve(), // node_modules
|
resolve({
|
||||||
|
extensions: [ '.mjs', '.js', '.jsx', '.json' ],
|
||||||
|
}), // node_modules
|
||||||
commonjs(), // CJS-modules
|
commonjs(), // CJS-modules
|
||||||
production && terser(), // minify, but only in production
|
production && terser(), // minify, but only in production
|
||||||
copy({
|
copy({
|
||||||
@ -43,7 +45,9 @@ export default [
|
|||||||
plugins: [
|
plugins: [
|
||||||
sass(),
|
sass(),
|
||||||
babel(), // babel
|
babel(), // babel
|
||||||
resolve(), // node_modules
|
resolve({
|
||||||
|
extensions: [ '.mjs', '.js', '.jsx', '.json' ],
|
||||||
|
}), // node_modules
|
||||||
commonjs(), // CJS-modules
|
commonjs(), // CJS-modules
|
||||||
production && terser(), // minify, but only in production
|
production && terser(), // minify, but only in production
|
||||||
copy({
|
copy({
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {defineElement, render, CustomElement} from "../../packages/csx-custom-elements/lib";
|
import {defineElement, render, CustomElement} from "../../packages/csx-custom-elements";
|
||||||
|
|
||||||
@defineElement('example-page')
|
@defineElement('example-page')
|
||||||
export class ExamplePage extends CustomElement{
|
export class ExamplePage extends CustomElement{
|
||||||
50
test/todos-mvc/components/my-todo.jsx
Normal file
50
test/todos-mvc/components/my-todo.jsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {defineElement, render, CustomElement, Host} from "../../../packages/csx-custom-elements";
|
||||||
|
|
||||||
|
import style from './my-todo.scss';
|
||||||
|
import {TodoInput} from './todo-input';
|
||||||
|
import {TodoItem} from './todo-item';
|
||||||
|
|
||||||
|
@defineElement('my-todo')
|
||||||
|
export class MyTodo extends CustomElement{
|
||||||
|
uid = 1;
|
||||||
|
todos = [
|
||||||
|
{id: this.uid++, text: "my initial todo", checked: false },
|
||||||
|
{id: this.uid++, text: "Learn about Web Components", checked: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<Host>
|
||||||
|
<style>{ style }</style>
|
||||||
|
<h1>CSX Todo</h1>
|
||||||
|
<section>
|
||||||
|
<todo-input onSubmit={this.handleSubmit}/>
|
||||||
|
<ul id="list-container"
|
||||||
|
onCheck={this.handleCheck}
|
||||||
|
onRemove={this.handleRemove}
|
||||||
|
>
|
||||||
|
{this.todos.map(item =>
|
||||||
|
<todo-item
|
||||||
|
model={ item.id }
|
||||||
|
checked={( item.checked )}
|
||||||
|
>{ item.text }</todo-item>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</Host>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = ({ detail: text }) => {
|
||||||
|
this.todos = [...this.todos, { id: this.uid++, text, checked: false }];
|
||||||
|
};
|
||||||
|
handleCheck = ({detail: checked}, id) => {
|
||||||
|
let indexOf = this.todos.findIndex(t=>t.id===id);
|
||||||
|
let updated = {...this.todos[indexOf], checked};
|
||||||
|
this.todos = [...this.todos.slice(0,indexOf), updated, ...this.todos.slice(indexOf+1)];
|
||||||
|
};
|
||||||
|
handleRemove = (e,id)=>{
|
||||||
|
let indexOf = this.todos.findIndex(t=>t.id===id);
|
||||||
|
this.todos = [...this.todos.slice(0,indexOf), ...this.todos.slice(indexOf+1)];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
test/todos-mvc/components/my-todo.scss
Normal file
24
test/todos-mvc/components/my-todo.scss
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 60px;
|
||||||
|
font-weight: 100;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(175, 47, 47, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: #fff;
|
||||||
|
margin: 30px 0 40px 0;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#list-container {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
border-top: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
35
test/todos-mvc/components/todo-input.jsx
Normal file
35
test/todos-mvc/components/todo-input.jsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {defineElement, render, CustomElement, Host} from "../../../packages/csx-custom-elements";
|
||||||
|
import style from './todo-input.scss';
|
||||||
|
|
||||||
|
@defineElement('todo-input')
|
||||||
|
export class TodoInput extends CustomElement{
|
||||||
|
value = "";
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<Host>
|
||||||
|
<style>{ style }</style>
|
||||||
|
<form onSubmit={ this.handleSubmit }>
|
||||||
|
<input
|
||||||
|
value={this.value}
|
||||||
|
type="text"
|
||||||
|
placeholder="What needs to be done?"
|
||||||
|
onInput={this.handleInput}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Host>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = (e)=>{
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.value) return;
|
||||||
|
this.dispatchEvent(new CustomEvent('submit', {
|
||||||
|
detail: this.value
|
||||||
|
}));
|
||||||
|
this.state = "";
|
||||||
|
};
|
||||||
|
handleInput = ({target: {value}})=>{
|
||||||
|
this.value = value;
|
||||||
|
};
|
||||||
|
}
|
||||||
29
test/todos-mvc/components/todo-input.scss
Normal file
29
test/todos-mvc/components/todo-input.scss
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
position: relative;
|
||||||
|
font-size: 24px;
|
||||||
|
border-bottom: 1px solid #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 16px 16px 16px 60px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(0, 0, 0, 0.003);
|
||||||
|
position: relative;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: 1.4em;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
color: inherit;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
37
test/todos-mvc/components/todo-item.jsx
Normal file
37
test/todos-mvc/components/todo-item.jsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {defineElement, render, CustomElement, Host, ShadowDOM} from "../../../packages/csx-custom-elements";
|
||||||
|
import style from './todo-item.scss';
|
||||||
|
|
||||||
|
@defineElement('todo-item')
|
||||||
|
export class TodoItem extends CustomElement{
|
||||||
|
checked = false;// TODO annotate as prop (attribute)
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<Host>
|
||||||
|
<ShadowDOM>
|
||||||
|
<style>{ style }</style>
|
||||||
|
<li class={( this.checked ? 'completed' : '' )}>
|
||||||
|
<input
|
||||||
|
type="checkbox" checked={ this.checked }
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
<label>
|
||||||
|
<slot />
|
||||||
|
</label>
|
||||||
|
<button onClick={this.handleClick}>x</button>
|
||||||
|
</li>
|
||||||
|
</ShadowDOM>
|
||||||
|
</Host>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = ()=>{
|
||||||
|
this.dispatchEvent(new CustomEvent('check', {
|
||||||
|
detail: (this.checked=!this.checked)
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
handleClick = ()=>{
|
||||||
|
this.dispatchEvent(new CustomEvent('remove', {
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
88
test/todos-mvc/components/todo-item.scss
Normal file
88
test/todos-mvc/components/todo-item.scss
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 24px;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 1px solid #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
|
li input {
|
||||||
|
text-align: center;
|
||||||
|
width: 40px;
|
||||||
|
/* auto, since non-WebKit browsers doesn't support input styling */
|
||||||
|
height: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 9px;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto 0;
|
||||||
|
border: none;
|
||||||
|
/* Mobile Safari */
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li input:after {
|
||||||
|
content: url('data:image/svg+xml;utf8,<svg%20xmlns%3D"http%3A//www.w3.org/2000/svg"%20width%3D"40"%20height%3D"40"%20viewBox%3D"-10%20-18%20100%20135"><circle%20cx%3D"50"%20cy%3D"50"%20r%3D"50"%20fill%3D"none"%20stroke%3D"%23ededed"%20stroke-width%3D"3"/></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
li input:checked:after {
|
||||||
|
content: url('data:image/svg+xml;utf8,<svg%20xmlns%3D"http%3A//www.w3.org/2000/svg"%20width%3D"40"%20height%3D"40"%20viewBox%3D"-10%20-18%20100%20135"><circle%20cx%3D"50"%20cy%3D"50"%20r%3D"50"%20fill%3D"none"%20stroke%3D"%23bddad5"%20stroke-width%3D"3"/><path%20fill%3D"%235dc2af"%20d%3D"M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z"/></svg>');
|
||||||
|
}
|
||||||
|
|
||||||
|
li label {
|
||||||
|
white-space: pre;
|
||||||
|
word-break: break-word;
|
||||||
|
padding: 15px 60px 15px 15px;
|
||||||
|
margin-left: 45px;
|
||||||
|
display: block;
|
||||||
|
line-height: 1.2;
|
||||||
|
transition: color 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.completed label {
|
||||||
|
color: #d9d9d9;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
li button,
|
||||||
|
li input[type="checkbox"] {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: baseline;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
color: inherit;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-font-smoothing: antialiased;
|
||||||
|
font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
li button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 10px;
|
||||||
|
bottom: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin: auto 0;
|
||||||
|
font-size: 30px;
|
||||||
|
color: #cc9a9a;
|
||||||
|
margin-bottom: 11px;
|
||||||
|
transition: color 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
li button:hover {
|
||||||
|
color: #af5b5e;
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import {render} from "../../packages/csx-custom-elements";
|
import {render} from "../../packages/csx-custom-elements";
|
||||||
import style from "./index.scss";
|
import style from "./index.scss";
|
||||||
|
import {MyTodo} from "./components/my-todo";
|
||||||
|
|
||||||
// Replace this with an example implementation of the Todos-MVC app
|
// Replace this with an example implementation of the Todos-MVC app
|
||||||
// look for inspiration here: https://github.com/shprink/web-components-todo
|
// look for inspiration here: https://github.com/shprink/web-components-todo
|
||||||
document.body.appendChild(render(<style>{style}</style>));
|
document.body.appendChild(render(<style>{style}</style>));
|
||||||
document.body.appendChild(render(<div class="center-me">
|
document.body.appendChild(render(<div class="center-me">
|
||||||
<h1>Todos MVC</h1>
|
<MyTodo />
|
||||||
</div>));
|
</div>));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user