diff --git a/packages/csx-custom-elements/src/custom-element/custom-element.js b/packages/csx-custom-elements/src/custom-element/custom-element.js
index 68f89ad..1ca307e 100644
--- a/packages/csx-custom-elements/src/custom-element/custom-element.js
+++ b/packages/csx-custom-elements/src/custom-element/custom-element.js
@@ -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(){
+
+ }
+}
diff --git a/packages/csx-custom-elements/src/vdom/constants.js b/packages/csx-custom-elements/src/vdom/constants.js
index c99fb5b..0ff4d60 100644
--- a/packages/csx-custom-elements/src/vdom/constants.js
+++ b/packages/csx-custom-elements/src/vdom/constants.js
@@ -15,4 +15,7 @@ export const VNODEPROP_EXCLUDE_DIRECT = {
export const VNODEPROP_IGNORE = {
['key']: true,
-};
\ No newline at end of file
+};
+
+export const Host = Symbol('host');
+export const ShadowDOM = Symbol('shadow-dom');
\ No newline at end of file
diff --git a/packages/csx-custom-elements/src/vdom/index.js b/packages/csx-custom-elements/src/vdom/index.js
index 919e1c7..22ef628 100644
--- a/packages/csx-custom-elements/src/vdom/index.js
+++ b/packages/csx-custom-elements/src/vdom/index.js
@@ -1,2 +1,3 @@
export * from "./vnode";
-export * from "./render";
\ No newline at end of file
+export * from "./render";
+export {Host, ShadowDOM} from "./constants";
\ No newline at end of file
diff --git a/packages/csx-custom-elements/src/vdom/render.js b/packages/csx-custom-elements/src/vdom/render.js
index aad01cf..3c3eaa3 100644
--- a/packages/csx-custom-elements/src/vdom/render.js
+++ b/packages/csx-custom-elements/src/vdom/render.js
@@ -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,61 +35,84 @@ 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);
-
- // 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 ];
- }
+ 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.style && typeof (vnode.props.style) === 'object') {
+ for (let styleKey in vnode.props.style) {
+ host.style[ styleKey ] = vnode.props.style[ styleKey ];
}
- for (let key in vnode.props) {
- let val = vnode.props[key];
- if(VNODEPROP_IGNORE[key]){
- // NO-OP
- }else if(VNODEPROP_DIRECT[key]){
+ }
+ for (let key in vnode.props) {
+ let val = vnode.props[key];
+ if(VNODEPROP_IGNORE[key]){
+ // NO-OP
+ }else if(VNODEPROP_DIRECT[key]){
+ host[key] = val;
+ }else{
+ if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){
host[key] = val;
- }else{
- if(!VNODEPROP_EXCLUDE_DIRECT[key] && !key.indexOf('-')){
- host[key] = val;
+ }
+ 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");
}
- if (val === false) {
+
+ }else {
+ if (val === false || val===null || val==='') {
host.removeAttribute(key);
} else if (val === true) {
host.setAttribute(key, "");
- } else{
+ } else {
host.setAttribute(key, val);
}
}
}
}
+ }
- // Children
- if (vnode.children) {
- let children = vnode.children instanceof Array? vnode.children : [vnode.children];
-
- for(let child of children){
- let el = child instanceof Element? child : render(child, {
- ...opts,
- host: undefined
- });
- if(el!==undefined){
- host.appendChild(el);
+ // 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);
}
+
}
}
-
- // 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;
diff --git a/rollup.config.js b/rollup.config.js
index 801eec3..b85f7fb 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -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({
diff --git a/test/basic/page.js b/test/basic/page.jsx
similarity index 91%
rename from test/basic/page.js
rename to test/basic/page.jsx
index da36748..446253c 100644
--- a/test/basic/page.js
+++ b/test/basic/page.jsx
@@ -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{
diff --git a/test/todos-mvc/components/my-todo.jsx b/test/todos-mvc/components/my-todo.jsx
new file mode 100644
index 0000000..3501d9a
--- /dev/null
+++ b/test/todos-mvc/components/my-todo.jsx
@@ -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 (
+ CSX Todo
+
+ {this.todos.map(item =>
+
+