Engine rebuild with user—side PhantomComponent class
Opened this issue · 1 comments
sidiousvic commented
Have tested this and it works. The result is a much more idiomatic user—side Phantom, similar to class—based React, but with key differences:
User lists children by returning an array from a state()
method
children() {
return [PhantomChild];
}
User defines state by returning a state object from a state()
method
state() {
return { message: "🍕" };
}
User defines markup by returning a template string from a render()
method
In this markup, the user...
- ...must define a
@componentName
attribute for wrapping element - ...can access child components via
this.<childComponentName>
- ...can access state via
this.<propertyInUserDefinedState>
render() {
return `
<div @app>
${this.Child}
<p>${this.message}</p>
</div>`;
}
User obtains a reference for every component from Phantom
export const { App, Child } = PHANTOM(PhantomApp);
User can access and update state via top—level component properties
document.addEventListener("click", toggleEmoji);
function toggleEmoji() {
if (Child.message === "💜")
Child.update({
message: "🍕",
});
else
Child.update({
message: "💜",
});
}
Prototype
///////////////////// 🔨 PHANTOMCOMPONENT
class PhantomComponent {
[x: string]: unknown;
data: any;
nest: any;
id: any;
constructor() {
this.data = {};
}
appear() {}
update(data: any) {
for (const [_k, v] of Object.entries(data)) {
this[_k] = v;
this.data[_k] = v;
}
this.appear();
}
}
///////////////////// ⚙️ PHANTOM ENGINE
function PHANTOM(Component: any, parent: any = undefined) {
injectPHANTOMElement();
const c = new Component();
if (parent) c.parent = parent;
c.name = removePhantomPrefixFromName(c);
const userDefinedState = c.state();
c.update(userDefinedState);
addUserDefinedChildrenToNest(c);
if (c.nest) {
const nestedApparitions = generateNestedApparitions(c);
c.update(nestedApparitions);
}
c.appear = () => updateNode(c);
const userDefinedHTML = c.render();
const componentNode = generateNode(userDefinedHTML);
if (!parent) document.body.append(componentNode);
return { [c.name]: c, ...c.nest };
}
///////////////////// 🧰 UTILITIES
function injectPHANTOMElement() {
if (!document.querySelector("#PHANTOM")) {
const PHANTOM = document.createElement("div");
PHANTOM.id = "PHANTOM";
document.body.appendChild(PHANTOM);
}
}
function removePhantomPrefixFromName(c: any) {
return c.constructor.name.replace("Phantom", "");
}
function addUserDefinedChildrenToNest(c: any) {
const nest: any = {};
if (c.children)
c.children().map((Child: any) => {
const childInstance = PHANTOM(Child, c);
for (const [_k, v] of Object.entries(childInstance)) {
nest[_k] = v;
}
});
c.nest = nest;
}
function generateNestedApparitions(c: any) {
const nestedApparitions: any = {};
const nest = c.nest;
for (const [_k] of Object.entries(nest)) {
const childComponent = nest[_k];
const childHtml = childComponent.render();
nestedApparitions[_k] = childHtml;
}
return nestedApparitions;
}
function getElementByPhantomId(phantomId: string) {
let element: Node | null = null;
function traverseNode(node: Node) {
if (node.childNodes) {
node.childNodes.forEach((childNode: ChildNode) => {
traverseNode(childNode);
});
}
if ((node as HTMLElement).attributes)
for (const [_k, v] of Object.entries((node as HTMLElement).attributes)) {
if (v.name === phantomId) element = node;
}
}
traverseNode(document.body);
return element;
}
function swapNode(swapIn: ChildNode, swapOut: ChildNode | null) {
swapOut?.replaceWith(swapIn);
return swapIn;
}
function parseNodeMap(html: string) {
return html.replace(/>,/g, ">");
}
function updateNode(c: any) {
// parse render() as a node
let html = c.render();
console.log(html);
const swapIn = generateNode(html);
console.log(c.name, getElementByPhantomId(`@${c.name.toLowerCase()}`));
let swapOut = getElementByPhantomId(`@${c.name.toLowerCase()}`);
console.log("SWAPIN:::", swapIn, "SWAPOUT:::", swapOut);
swapNode(swapIn as HTMLElement, swapOut);
}
function generateNode(html: string) {
html = parseNodeMap(html); // sanitize HTML commas
let doc = new DOMParser().parseFromString(html, "text/html");
return doc.body.firstChild as HTMLElement;
}
///////////////////// 💻 USER SIDE
class PhantomChild extends PhantomComponent {
state() {
return { message: "💜", hearts: [1, 2, 3] };
}
render() {
return `
<div @child>
${(this.hearts as string[]).map(() => `<p>${this.message}</p>`)}
</div>`;
}
}
class PhantomApp extends PhantomComponent {
children() {
return [PhantomChild];
}
state() {
return { message: "🍕" };
}
render() {
return `
<div @app>
${this.Child}
<p>${this.message}</p>
</div>`;
}
}
export const { App, Child } = PHANTOM(PhantomApp);
console.log("Components 😈:", App, Child);
document.addEventListener("click", toggleEmoji);
function toggleEmoji() {
if (Child.message === "💜")
Child.update({
message: "🍕",
});
else
Child.update({
message: "💜",
});
}
sidiousvic commented
@nayelyrodarte check this baby out when you can! No work needed, just familiarize yourself and don't hesitate to share your opinion or ask any questions.