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:
|
||||
* 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)
|
||||
*/
|
||||
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 = {
|
||||
['key']: true,
|
||||
};
|
||||
|
||||
export const Host = Symbol('host');
|
||||
export const ShadowDOM = Symbol('shadow-dom');
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./vnode";
|
||||
export * from "./render";
|
||||
export {Host, ShadowDOM} from "./constants";
|
||||
@ -1,5 +1,6 @@
|
||||
import './vnode';
|
||||
import {
|
||||
Host, ShadowDOM,
|
||||
VNODE_EXCLUDE,
|
||||
VNODEPROP_DIRECT, VNODEPROP_EXCLUDE_DIRECT,
|
||||
VNODEPROP_IGNORE,
|
||||
@ -21,7 +22,11 @@ import {
|
||||
* @return {Element}
|
||||
*/
|
||||
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 {
|
||||
/**
|
||||
* @type {Element}
|
||||
@ -30,15 +35,21 @@ export function render(vnode, opts = {}) {
|
||||
} = opts;
|
||||
|
||||
if(VNODE_EXCLUDE[vnode]) return undefined;
|
||||
console.log(vnode);
|
||||
|
||||
if(vnode instanceof Object){
|
||||
// Type
|
||||
let tagName = vnode.type instanceof Object? vnode.type.tagName : vnode.type;
|
||||
if(!host) host = document.createElement(tagName);
|
||||
if(!host){
|
||||
if(!['object', 'function', 'symbol'].includes(typeof(vnode))){
|
||||
host = document.createTextNode(vnode);
|
||||
}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
|
||||
if (vnode.props) {
|
||||
if (vnode?.props) {
|
||||
if (vnode.props.style && typeof (vnode.props.style) === 'object') {
|
||||
for (let styleKey in vnode.props.style) {
|
||||
host.style[ styleKey ] = vnode.props.style[ styleKey ];
|
||||
@ -54,7 +65,19 @@ export function render(vnode, opts = {}) {
|
||||
if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){
|
||||
host[key] = val;
|
||||
}
|
||||
if (val === false) {
|
||||
if(key.slice(0,2)==='on' && key[2]>='A' && key[2]<='Z'){
|
||||
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");
|
||||
}
|
||||
|
||||
}else {
|
||||
if (val === false || val===null || val==='') {
|
||||
host.removeAttribute(key);
|
||||
} else if (val === true) {
|
||||
host.setAttribute(key, "");
|
||||
@ -64,27 +87,32 @@ export function render(vnode, opts = {}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Children
|
||||
if (vnode.children) {
|
||||
let children = vnode.children instanceof Array? vnode.children : [vnode.children];
|
||||
|
||||
for(let child of children){
|
||||
if (vnode?.children) {
|
||||
let queue = vnode.children instanceof Array? vnode.children.slice() : [vnode.children];
|
||||
while(queue.length){
|
||||
let child = queue.splice(0,1)[0];
|
||||
if(child instanceof Array){
|
||||
queue.splice(0,0,...child);
|
||||
}else{
|
||||
if(child?.type === ShadowDOM){
|
||||
let shadow = host.attachShadow({mode: 'open'});
|
||||
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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
@ -21,7 +21,9 @@ export default [
|
||||
plugins: [
|
||||
sass(),
|
||||
babel(), // babel
|
||||
resolve(), // node_modules
|
||||
resolve({
|
||||
extensions: [ '.mjs', '.js', '.jsx', '.json' ],
|
||||
}), // node_modules
|
||||
commonjs(), // CJS-modules
|
||||
production && terser(), // minify, but only in production
|
||||
copy({
|
||||
@ -43,7 +45,9 @@ export default [
|
||||
plugins: [
|
||||
sass(),
|
||||
babel(), // babel
|
||||
resolve(), // node_modules
|
||||
resolve({
|
||||
extensions: [ '.mjs', '.js', '.jsx', '.json' ],
|
||||
}), // node_modules
|
||||
commonjs(), // CJS-modules
|
||||
production && terser(), // minify, but only in production
|
||||
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')
|
||||
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 style from "./index.scss";
|
||||
import {MyTodo} from "./components/my-todo";
|
||||
|
||||
// Replace this with an example implementation of the Todos-MVC app
|
||||
// look for inspiration here: https://github.com/shprink/web-components-todo
|
||||
document.body.appendChild(render(<style>{style}</style>));
|
||||
document.body.appendChild(render(<div class="center-me">
|
||||
<h1>Todos MVC</h1>
|
||||
<MyTodo />
|
||||
</div>));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user