phantom icon indicating copy to clipboard operation
phantom copied to clipboard

Engine rebuild with user—side PhantomComponent class

Open sidiousvic opened this issue 4 years ago • 1 comments

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 avatar Jul 27 '20 23:07 sidiousvic

@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.

sidiousvic avatar Jul 27 '20 23:07 sidiousvic