diff --git a/src/component.js b/src/component.js index 3d7b55d..e29abae 100644 --- a/src/component.js +++ b/src/component.js @@ -1 +1,26 @@ -export default class Component {} +import { reconcile } from "./reconciler"; + +export class Component { + constructor(props) { + this.props = props; + this.state = this.state || {}; + } + + setState(partialState) { + this.state = Object.assign({}, this.state, partialState); + updateInstance(this.__internalInstance); + } +} + +function updateInstance(internalInstance) { + const parentDom = internalInstance.dom.parentNode; + const element = internalInstance.element; + reconcile(parentDom, internalInstance, element); +} + +export function createPublicInstance(element, internalInstance) { + const { type, props } = element; + const publicInstance = new type(props); + publicInstance.__internalInstance = internalInstance; + return publicInstance; +} diff --git a/src/didact.js b/src/didact.js index 69663fc..501aad6 100644 --- a/src/didact.js +++ b/src/didact.js @@ -1,15 +1,11 @@ -import { createElement } from './element'; -import Component from './component'; -import { render } from './reconciler'; +import { createElement } from "./element"; +import { Component } from "./component"; +import { render } from "./reconciler"; export default { - createElement, - Component, - render + createElement, + Component, + render }; -export { - createElement, - Component, - render -}; +export { createElement, Component, render }; diff --git a/src/element.js b/src/element.js index 3b45acf..88fe167 100644 --- a/src/element.js +++ b/src/element.js @@ -1,4 +1,4 @@ -const TEXT_ELEMENT = "TEXT ELEMENT"; +export const TEXT_ELEMENT = "TEXT ELEMENT"; export function createElement(type, config, ...args) { const props = Object.assign({}, config); diff --git a/src/reconciler.js b/src/reconciler.js index 4b93552..cfd5377 100644 --- a/src/reconciler.js +++ b/src/reconciler.js @@ -1,4 +1,6 @@ import { updateDomProperties } from "./dom-utils"; +import { TEXT_ELEMENT } from "./element"; +import { createPublicInstance } from "./component"; let rootInstance = null; @@ -8,7 +10,7 @@ export function render(element, container) { rootInstance = nextInstance; } -function reconcile(parentDom, instance, element) { +export function reconcile(parentDom, instance, element) { if (instance == null) { // Create instance const newInstance = instantiate(element); @@ -18,17 +20,27 @@ function reconcile(parentDom, instance, element) { // Remove instance parentDom.removeChild(instance.dom); return null; - } else if (instance.element.type === element.type) { - // Update instance + } else if (instance.element.type !== element.type) { + // Replace instance + const newInstance = instantiate(element); + parentDom.replaceChild(newInstance.dom, instance.dom); + return newInstance; + } else if (typeof element.type === "string") { + // Update dom instance updateDomProperties(instance.dom, instance.element.props, element.props); instance.childInstances = reconcileChildren(instance, element); instance.element = element; return instance; } else { - // Replace instance - const newInstance = instantiate(element); - parentDom.replaceChild(newInstance.dom, instance.dom); - return newInstance; + //Update composite instance + instance.publicInstance.props = element.props; + const childElement = instance.publicInstance.render(); + const oldChildInstance = instance.childInstance; + const childInstance = reconcile(parentDom, oldChildInstance, childElement); + instance.dom = childInstance.dom; + instance.childInstance = childInstance; + instance.element = element; + return instance; } } @@ -49,21 +61,33 @@ function reconcileChildren(instance, element) { function instantiate(element) { const { type, props } = element; + const isDomElement = typeof type === "string"; - // Create DOM element - const isTextElement = type === "TEXT ELEMENT"; - const dom = isTextElement - ? document.createTextNode("") - : document.createElement(type); + if (isDomElement) { + // Instantiate DOM element + const isTextElement = type === TEXT_ELEMENT; + const dom = isTextElement + ? document.createTextNode("") + : document.createElement(type); - updateDomProperties(dom, [], props); + updateDomProperties(dom, [], props); - // Instantiate and append children - const childElements = props.children || []; - const childInstances = childElements.map(instantiate); - const childDoms = childInstances.map(childInstance => childInstance.dom); - childDoms.forEach(childDom => dom.appendChild(childDom)); + const childElements = props.children || []; + const childInstances = childElements.map(instantiate); + const childDoms = childInstances.map(childInstance => childInstance.dom); + childDoms.forEach(childDom => dom.appendChild(childDom)); - const instance = { dom, element, childInstances }; - return instance; + const instance = { dom, element, childInstances }; + return instance; + } else { + // Instantiate component element + const instance = {}; + const publicInstance = createPublicInstance(element, instance); + const childElement = publicInstance.render(); + const childInstance = instantiate(childElement); + const dom = childInstance.dom; + + Object.assign(instance, { dom, element, childInstance, publicInstance }); + return instance; + } } diff --git a/test/04.components.test.js b/test/04.components.test.js new file mode 100644 index 0000000..6a540cc --- /dev/null +++ b/test/04.components.test.js @@ -0,0 +1,39 @@ +import test from "ava"; +import browserEnv from "browser-env"; +/** @jsx createElement */ +import { render, createElement, Component } from "../src/didact"; + +// Create document global var +browserEnv(["document"]); + +test.beforeEach(t => { + let root = document.getElementById("root"); + if (!root) { + root = document.createElement("div"); + root.id = "root"; + document.body.appendChild(root); + } + t.context.root = root; +}); + +test("render component", t => { + const root = t.context.root; + class FooComponent extends Component { + render() { + return
; + } + } + render(, root); + t.is(root.innerHTML, '
'); +}); + +test("render component with props", t => { + const root = t.context.root; + class FooComponent extends Component { + render() { + return
{this.props.name}
; + } + } + render(, root); + t.is(root.innerHTML, '
Bar
'); +}); diff --git a/test/05.set-state.test.js b/test/05.set-state.test.js new file mode 100644 index 0000000..34e1f28 --- /dev/null +++ b/test/05.set-state.test.js @@ -0,0 +1,49 @@ +import test from "ava"; +import browserEnv from "browser-env"; +/** @jsx createElement */ +import { render, createElement, Component } from "../src/didact"; + +// Create document global var +browserEnv(["document"]); + +test.beforeEach(t => { + let root = document.getElementById("root"); + if (!root) { + root = document.createElement("div"); + root.id = "root"; + document.body.appendChild(root); + } + t.context.root = root; +}); + +test("change state on click", t => { + const root = t.context.root; + class FooComponent extends Component { + constructor(props) { + super(props); + this.state = { + count: 0 + }; + } + + handleClick() { + this.setState({ + count: this.state.count + 1 + }); + } + + render() { + return
this.handleClick()}>{this.state.count}
; + } + } + render(, root); + t.is(root.innerHTML, "
0
"); + click(root.firstChild); + t.is(root.innerHTML, "
1
"); +}); + +function click(dom) { + var evt = document.createEvent("MouseEvent"); + evt.initEvent("click", false, true); + dom.dispatchEvent(evt); +}