Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC Validation #6

Open
wants to merge 52 commits into
base: master
Choose a base branch
from
Open

POC Validation #6

wants to merge 52 commits into from

Conversation

SRNV
Copy link
Contributor

@SRNV SRNV commented Nov 13, 2020

Eon POC validation

note: the work done is only for SPA

this pull request aims to validate the concept of Single File TSX/JSX Components. it continues the work done in #5.
the main goal was to be able to use jsx features without using VDOM, performance in mind.
for this many solutions were needed.
there's some informations to take in account.
JSX is mostly designed for React's usage. this implies some technical constraints like props typechecking, props evaluations, component importations, direct evaluation or deferred evaluation.

how to run ?

sudo deno run -A --unstable examples/todo-list/mod.ts

Additions

Module Identification

for components resolutions, export const name = '...' were required for all components.
this was made for components identifications into the DOM tree.
I mean when a component A used a component B named component-b, the end user had to do:

// ./A.ts
export default function(this: VMC) {
  return (<template>
    <component-b />
  </template>)
}

knowing that jsx has two way to declare elements : IntrinsicsElements and Value-based Elements. this approach makes all components considered as IntrinsicsElements.

this creates some weaknesses into the development experience because IntrinsicsElements doesn't allow property evaluations/ typechecking.
it means that if <component-b /> requires a property message, the end user will only know it into the runtime.
the end user should be able to identify type errors in the IDE and in the compiler time (Deno for instance).

so now all the components are Value-based Elements.

// ./A.ts
import ComponentB from './B.tsx';
export default function(this: VMC) {
  return (<template>
    <ComponentB /> // error: property message is required in type '{ message: string }'
  </template>)
}

EonSandBox

the main addition is the new class EonSandBox (name can be discussed). the idea is to copy the whole current directory into a new directory named .eon/. this one is at the same level.
what this approach offers:

  • better module resolutions made once
  • types no-check
  • jsx factory addition on the compiler time
  • file additions without touching the current working directory (for Compiler APIs)

ModuleGetter deletion

this class is removed since EonSandBox is doing the same job but once.

DOMElementDescriber (SWC parser)

the SWC Parser is used (as a dependency) to get some ASTs.
sadly Deno doesn't expose an ast method 'cause there's no internal consensus. this is why the swc parser is used through the nestland module deno_swc. there's some issues into it and the maintainability is debatable. but...
this one is useful for the list rendering, I mean the Vue: v-for or Svelte: {%each %} statements.

the idea was to be able to get all the end user's inputs, evaluated by Typescript and avoid using the VDOM solution (generate all the list, compare with the previous list, discard useless changes, etc...)

Patterns

this Pull request uses pattern files to compile part of code.
this means that we read the file and change template values with data.
please check the folder src/patterns/ to see what is implemented
this solution is debatable.

Web-Components

Web-Components are used and defined into the runtime

Design choices

Life cycle

this part is debatable, this Pull request includes some Life cycle hooks:

  • connected
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed

the end user would have to set those life cycles has static methods of the class VMC. like following

export default function(this: VMC) {
  return (<template>
    {() => this.value}
  </template>)
}
export class VMC {
  value: string = 'test';

  // fired when the web-components uses connectedCallback
  static connected(this: VMC);

  // when there's a reaction into the component
  // and before any update
  static beforeUpdate(this: VMC);

  // when there's a reaction into the component
  // after all updates made
  static updated(this: VMC);
  // before any removal when a web-component uses disconnectedCallback 
  static beforeDestroy(this: VMC);
  //  after all removal when a web-component uses disconnectedCallback 
  static destroyed(this: VMC);
}

maybe we can rename some for consistency like: beforeDestroy | desroyed.

Props

IMO props type checking is mandated, so the choice made is to require an export default for each Single File TSX/JSX components, like Typescript mention it in it's doc, the first argument of a component is used for props definition.

export default function(this: VMC, props: { property: string }) {
  return (<template>
    <form>
      <input value={() => this.value}></input>
      <button>Add</button>
    </form>
  </template>)
}

the constraint is, since we are not re-using the export default on every reactions, we can't reassign props
the first choice made was to add a static method to the class VMC which would handle props reactions

interface PropsType { property: string }
export default function(this: VMC, props: PropsType) {
  return (<template>
    <form>
      <input value={() => this.value}></input>
      <button>Add</button>
    </form>
  </template>)
}
export class VMC {
  value: string = '';
  static props(this: VMC, props: PropsType) {
    this.value = props.property;
  }
}

Reactivity

the reactivity solution is Proxy based, for deep observations of nested objects/arrays
there's many way to implement reactivity:

  • helpers (React's way: setValue, setName, setState etc...)
  • proxies (Vue 3 's way)
  • setters/getters (old Vue's way)
  • callback addition (Svelte's way with $$invalidate)
    I decided to implement the Proxies because some time you will need to pass the component's vmc into an external module myModule(this); and nobody knows what will happen to the data of the component.

Svelte's approach requires to parse the AssignmentPattern, in every files, every modules or to make an assignment after each suspected modifications, + arrays aren't reactive.

React's approach requires to pass the helper everywhere.

export default function(this: VMC) {
  return (<template>
    <form>
      <input value={() => this.value}></input>
      <button>Add</button>
    </form>
  </template>)
}
export class VMC {
  value: string = '';
  static connected(this: VMC) {
    this.value = 'text should change'
  }
}

List rendering

list rendering is the most complex feature for a front-end framework. keeping in mind that we have to keep focus on performance. Performance become a thing when you will have to render, update, remove 1000+ items, the solution can quickly be expansive in term of performance.
basically list rendering is why we shouldn't use VDOM. knowing that every time there is a change all the list is re-rendered and compared to the previous DOM (think you have 10 000 objects).
so what's the solution Eon is using

React

// ./TodoListApp.tsx
import React, { useState } from '..';
import TodoListRow, { Todo } from './TodoListRow.tsx';

export default () => {
  const [list, setList] = useState([]);
  return (<>
    {list.map((todo: Todo, i: number, arr: Todo[]) => 
      <TodoListRow todo={todo}>
        <span>
          {`${i}  -  `'}
        </span>
      </TodoListRow>
    )}
  </>)
}

first regarding this code, you can read that everytime setList(Array.from(new Array(10 000))) is used, React will update the DOM by using again the function, this basically implies another call to useState, another array creation and the re-rendering of list.map(...)

Eon

// ./TodoListApp.tsx
import TodoListRow, { Todo } from './TodoListRow.tsx';
 
export default function(this: VMC) {
  return (<template>
    {(todo: Todo, i: number, arr: Todo[] = this.list) =>
          <TodoListRow todo={() => todo}>
            <span>
              {() => `${i}  -  `'}
            </span>
          </TodoListRow>
        }
  </template>)
}
export class VMC {
  list: Todo[] = []
}

the main difference is that Eon uses once the function. why once ? just to get the DOM tree and update the DOM surgically.
while walking into the DOM, if it catch a dynamic list (we need to find a name) , the list will be transpiled and wrapped into an element ( need improvements) and then whenever the end user set a new list, the length of the new list and element.children.length are compared. if it's bigger new elements are created one by one, if it's smaller elements are removed one by one.

Ideas

Eon could use prototypes instead of class VMC:

// ./ideas/design-prototype.tsx
import TodoListRow, { Todo } from '../todo-list/TodoListRow.tsx';

export interface DesignPrototypeProps { message: string }
export interface DesignPrototype {
  list: Todo[];
  props: DesignPrototypeProps
}

export default function(this: DesignPrototype, props: DesignPrototypeProps) {
  this.list = []; // reactive list
  this.props = props; // now reactive
  return (<template>
    {() => this.props.message}
    {(todo: Todo, i: number, arr: Todo[] = this.list) =>
      <TodoListRow todo={() => todo}>
        <span>
          {() => `${i}  -  `}
        </span>
      </TodoListRow>
    }
  </template>)
}

this idea allows a lighter component. instead of exporting two important things template function & class VMC; you will only have to export default one prototype function.

how can Eon use prototype functions ?

considering the export default as DefaultPrototype

import DefaultPrototype from '...'
// ...
const component = reactive(new DefaultPrototype({ ... }));

Todos

  • parent - child components communication - a parent can pass props to a child but a child can't pass data to it's parent
  • implement partial updates instead of global updates
  • add getInnerHTML/getOuterHTML to DOMElement
  • SSR implementation

Known Issues

  • Dev Server can sometime use a used port for server
  • nested for directives are not rendered
  • in static props when the end user tries to assign a prop there is no reaction: this.todo = props.todo // no reaction

@SRNV SRNV self-assigned this Nov 13, 2020
@SRNV SRNV added the poc label Nov 13, 2020
src/classes/DOMElementDescriber.ts Show resolved Hide resolved
src/classes/ModuleErrors.ts Show resolved Hide resolved
src/functions/runtime.ts Outdated Show resolved Hide resolved
@SRNV SRNV removed the request for review from martonlederer November 14, 2020 15:55
@SRNV SRNV requested a review from littledivy November 16, 2020 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants