Skip to content

Commit

Permalink
feature/vdom-inside-component: Хранение виртуального и реального DOM …
Browse files Browse the repository at this point in the history
…в компоненте (#4)

* feat: add common types as declaration

* refactor: correct types

* refactor: rename "VirtualDomNode"

* feat: add common interface for virtual-dom nodes

* feat: add virtual-dom component node

* refactor: add any component type

* refactor: move nodes in folder

* feat: add support component node factory

* feat: inject virtual and real dom in component

* feat: add update component

* fix: check base element cache
  • Loading branch information
yoyopokki authored Jun 29, 2022
1 parent 41d7e9e commit 114a2d7
Show file tree
Hide file tree
Showing 13 changed files with 224 additions and 206 deletions.
50 changes: 28 additions & 22 deletions src/component/index.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,85 @@
import Subscriber from '@/subscriber';
import VirtualDomNode from '@/virtual-dom/virtual-dom-node';
import subscribeOnChange from '@/utils/subscribe-on-change';
import VirtualDom from '@/virtual-dom';
import { VirtualDomNode } from '@/virtual-dom/types';

abstract class Component<
P extends Record<string, unknown>,
S extends Record<string, unknown>
P extends MaybeEmptyObject<AnyObject>,
S extends MaybeEmptyObject<AnyObject>
> {
private _initialProps: Partial<P> = {};
private _virtualDom: VirtualDom = new VirtualDom();

private _props: Partial<P> = {};
private _state: Partial<S> = {};

private _subscribersOnUpdate = new Subscriber();

constructor(props: P) {
this._initialProps = props;
this._props = props;
}

protected get props() {
protected get props(): Partial<P> {
return this._props;
}

protected set props(value: Partial<P>) {
this._props = subscribeOnChange<P>(value, () => this.update());
}

protected get state() {
protected get state(): Partial<S> {
return this._state;
}

protected set state(value: Partial<S>) {
this._state = subscribeOnChange<S>(value, () => this.update());
}

public create() {
public get baseElement(): isNullable<HTMLElement> {
return this._virtualDom.realDom.baseElement;
}

public create(): void {
this.onCreateStart();

this.props = this._initialProps;
this._virtualDom.patch(this.render());

this.onCreateEnd();
}

public subscribeOnUpdate(subscriber: () => void) {
this._subscribersOnUpdate.subscribe(subscriber);
public mount(parentElement: HTMLElement): void {
this.onMountStart();

this._virtualDom.realDom.mount(parentElement);

this.onMountEnd();
}

protected update(): void {
public update(): void {
this.onUpdateStart();

this._subscribersOnUpdate.notify();
this._virtualDom.patch(this.render());

this.onUpdateEnd();
}

public onCreateStart(): void {
protected onCreateStart(): void {
return;
}

public onCreateEnd(): void {
protected onCreateEnd(): void {
return;
}

public onMountStart(): void {
protected onMountStart(): void {
return;
}

public onMountEnd(): void {
protected onMountEnd(): void {
return;
}

public onUpdateStart(): void {
protected onUpdateStart(): void {
return;
}

public onUpdateEnd(): void {
protected onUpdateEnd(): void {
return;
}

Expand Down
7 changes: 3 additions & 4 deletions src/component/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Component from '@/component';

export type ComponentConstructor = new (...unknown) => Component<
Record<string, unknown>,
Record<string, unknown>
>;
export type ComponentConstructor = new (...unknown) => AnyComponent;

export type AnyComponent = Component<AnyObject, AnyObject>;
20 changes: 5 additions & 15 deletions src/generation-plov/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
import elementFactory from '@/utils/element-factory';
import RealDom from '@/real-dom';
import VirtualDom from '@/virtual-dom';
import VirtualDomNode from '@/virtual-dom/virtual-dom-node';
import { VirtualDomNode } from '@/virtual-dom/types';

class GenerationPlov {
private _virtualDom: VirtualDom;
private _realDom: RealDom;
private _virtualDom = new VirtualDom();

constructor(
private _rootNode: VirtualDomNode,
private _rootElement: HTMLElement | null
private _rootElement: isNullable<HTMLElement>
) {
if (!this._rootElement) {
throw new Error('rootElement not found');
}

this._virtualDom = new VirtualDom(this._rootNode, this._onNodeUpdate);

this._realDom = new RealDom(this._rootElement);
if (this._virtualDom.tree) {
this._realDom.mountRoot(this._virtualDom.tree);
}
this._virtualDom.patch(this._rootNode);
this._virtualDom.realDom.mount(this._rootElement);
}

private _onNodeUpdate = (node: VirtualDomNode) => {
this._realDom.mount(node);
};

public static elementFactory = elementFactory;
}

Expand Down
103 changes: 48 additions & 55 deletions src/real-dom/index.ts
Original file line number Diff line number Diff line change
@@ -1,86 +1,79 @@
import VirtualDomNode from '@/virtual-dom/virtual-dom-node';
import VirtualDomComponentNode from '@/virtual-dom/nodes/virtual-dom-component-node';
import VirtualDomElementNode from '@/virtual-dom/nodes/virtual-dom-element-node';
import { VirtualDomNode } from '@/virtual-dom/types';

class RealDom {
private _elementMap: Map<symbol, HTMLElement> = new Map();
private _baseElement: isNullable<HTMLElement> = null;

constructor(private _rootElement: HTMLElement) {}

private _removeTails(node: VirtualDomNode): void {
const { key, children } = node;

this._elementMap.delete(key);

children.forEach((childNode) => {
if (childNode instanceof VirtualDomNode) {
this._removeTails(childNode);
}
});
public get baseElement(): isNullable<HTMLElement> {
return this._baseElement;
}

private _getElementOrGenerate(node: VirtualDomNode): HTMLElement {
const { key, tagName } = node;
private _baseElementFactory(node: VirtualDomNode): HTMLElement {
const { children, props } = node;

let element;
const element = this._baseElement || this._domElementFactory(node);

if (this._elementMap.get(key)) {
element = this._elementMap.get(key);
} else {
element = document.createElement(tagName);

this._elementMap.set(key, element);
if (this._baseElement) {
const childNodes = Array.from(element.childNodes);
childNodes.forEach((node) => node.remove());
}

return element;
}

private _domElementFactory(node: VirtualDomNode): HTMLElement {
const { props, children } = node;

const element = this._getElementOrGenerate(node);

for (const [prop, value] of Object.entries(props)) {
element.setAttribute(prop, value);
}

const childElements = Array.from(element.childNodes);
childElements.forEach((element) => element.remove());

children.forEach((childNode) => {
if (childNode instanceof VirtualDomNode) {
this._removeTails(childNode);
if (typeof childNode === 'string') {
element.appendChild(document.createTextNode(childNode));

return;
}

if (childNode instanceof VirtualDomElementNode) {
element.appendChild(this._domElementFactory(childNode));
}

if (childNode instanceof VirtualDomComponentNode) {
const { component } = childNode;

component.mount(element);
}
});

return element;
}

public mountRoot(node: VirtualDomNode) {
this.mount(node, this._rootElement);
}
private _domElementFactory(node: VirtualDomNode): HTMLElement {
let element: HTMLElement;

public mount(node: VirtualDomNode, parentElement?: HTMLElement) {
const { children, component } = node;
const fallbackElement = document.createElement('div');

const element = this._domElementFactory(node);
if (node instanceof VirtualDomElementNode) {
const { tagName } = node;

children.forEach((childNode) => {
if (childNode instanceof VirtualDomNode) {
this.mount(childNode, element);
element = document.createElement(tagName);
} else if (node instanceof VirtualDomComponentNode) {
const { component } = node;

return;
}
element = component.baseElement || fallbackElement;
} else {
element = fallbackElement;
}

const childTextElement = document.createTextNode(childNode);
element.appendChild(childTextElement);
});
return element;
}

if (parentElement) {
component && component.onMountStart();
public mount(parentElement: HTMLElement) {
if (!this._baseElement) {
return;
}

parentElement.appendChild(element);
parentElement.appendChild(this._baseElement);
}

component && component.onMountEnd();
}
public patchBaseElement(node: VirtualDomNode) {
this._baseElement = this._baseElementFactory(node);
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/test-components/header.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Component from '@/component';
import GenerationPlov from '@/generation-plov';
import VirtualDomNode from '@/virtual-dom/virtual-dom-node';
import { VirtualDomNode } from '@/virtual-dom/types';

class Header extends Component<Record<string, never>, { title: string }> {
constructor(props: Record<string, never>) {
class Header extends Component<EmptyObject, { title: string }> {
constructor(props: EmptyObject) {
super(props);

this.state = {
Expand Down
9 changes: 9 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type StringObject = Record<string, string>;

type AnyObject = Record<string, unknown>;

type EmptyObject = Record<string, never>;

type isNullable<T> = T | null;

type MaybeEmptyObject<T extends AnyObject> = T | EmptyObject;
27 changes: 8 additions & 19 deletions src/utils/element-factory.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import { ComponentConstructor } from '@/component/types';
import VirtualDomNode from '@/virtual-dom/virtual-dom-node';

const componentFactory = (
Component: ComponentConstructor,
props: Record<string, string>
): VirtualDomNode => {
const component = new Component(props);
component.create();

const virtualDomNode = component.render();
virtualDomNode.component = component;

return virtualDomNode;
};
import VirtualDomComponentNode from '@/virtual-dom/nodes/virtual-dom-component-node';
import VirtualDomElementNode from '@/virtual-dom/nodes/virtual-dom-element-node';
import { VirtualDomNode, VirtualDomNodeChild } from '@/virtual-dom/types';

const elementFactory = (
tagName: string | ComponentConstructor,
props: Record<string, string>,
children: (VirtualDomNode | string)[]
props: StringObject,
children: VirtualDomNodeChild[]
): VirtualDomNode => {
if (typeof tagName !== 'string') {
return componentFactory(tagName, props);
if (typeof tagName === 'function') {
return new VirtualDomComponentNode(tagName, props, children);
}

return new VirtualDomNode(tagName, props, children);
return new VirtualDomElementNode(tagName, props, children);
};

export default elementFactory;
2 changes: 1 addition & 1 deletion src/utils/subscribe-on-change.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const subscribeOnChange = <T extends Record<string, unknown>>(
const subscribeOnChange = <T extends AnyObject>(
target: Partial<T>,
callback: () => void
): Partial<T> => {
Expand Down
Loading

0 comments on commit 114a2d7

Please sign in to comment.