diff --git a/src/Component/Component.js b/src/Component/Component.js new file mode 100644 index 0000000..80e26e7 --- /dev/null +++ b/src/Component/Component.js @@ -0,0 +1,4 @@ +export function Component(props = {}) { + this.props = props; + this.render = () => {}; +} diff --git a/src/Component/index.js b/src/Component/index.js new file mode 100644 index 0000000..e51a5d2 --- /dev/null +++ b/src/Component/index.js @@ -0,0 +1 @@ +export * from './Component'; diff --git a/src/Node/Node.js b/src/Node/Node.js new file mode 100644 index 0000000..6061c24 --- /dev/null +++ b/src/Node/Node.js @@ -0,0 +1,5 @@ +export function Node(element, attributes, children) { + this.element = element; + this.attributes = attributes; + this.children = children; +} diff --git a/src/Node/Node.test.js b/src/Node/Node.test.js new file mode 100644 index 0000000..2f26808 --- /dev/null +++ b/src/Node/Node.test.js @@ -0,0 +1,13 @@ +/* eslint-env jest */ +/** @jsx createElement */ + +import { Node } from './Node'; + +describe('Node', () => { + test('Should create a node with provided parameters', () => { + const node = new Node(1, 2, 3); + expect(node.element).toBe(1); + expect(node.attributes).toBe(2); + expect(node.children).toBe(3); + }); +}); diff --git a/src/Node/index.js b/src/Node/index.js new file mode 100644 index 0000000..673cbdf --- /dev/null +++ b/src/Node/index.js @@ -0,0 +1 @@ +export * from './Node'; diff --git a/src/createElement/createElement.js b/src/createElement/createElement.js new file mode 100644 index 0000000..d5625fc --- /dev/null +++ b/src/createElement/createElement.js @@ -0,0 +1,5 @@ +import { Node } from '../Node'; + +export function createElement(element, attributes, ...children) { + return new Node(element, attributes, children); +} diff --git a/src/createElement/createElement.test.js b/src/createElement/createElement.test.js new file mode 100644 index 0000000..9b97834 --- /dev/null +++ b/src/createElement/createElement.test.js @@ -0,0 +1,87 @@ +/* eslint-env jest */ +/** @jsx createElement */ + +import { Node } from '../Node'; +import { createElement } from './createElement'; + +describe('createElement()', () => { + test('Should create node tree with provided data structure', () => { + const withStr = 'over here'; + const withNum = 100; + const tree = ( +
+

Hello!

+
Awesome {withStr} fun {withNum}!
+
+ ); + + expect(tree).toBeInstanceOf(Node); + + expect(tree.children.length).toBe(2); + expect(tree.children[0]).toBeInstanceOf(Node); + expect(tree.children[0].children.length).toBe(1); + expect(tree.children[0].children[0]).toBe('Hello!'); + + expect(tree.children[1]).toBeInstanceOf(Node); + expect(tree.children[1].children.length).toBe(5); + + expect(tree.children[1].children[0]).toBeString(); + expect(tree.children[1].children[1]).toBeString(); + expect(tree.children[1].children[2]).toBeString(); + + expect(tree.children[1].children[3]).toBeInstanceOf(Node); + expect(tree.children[1].children[3].children.length).toBe(2); + expect(tree.children[1].children[3].children[0]).toBeString(); + expect(tree.children[1].children[3].children[1]).toBeNumber(); + + expect(tree.children[1].children[4]).toBeString(); + }); + + test('Should create node tree with provided props', () => { + const withStr = 'over here'; + const withNum = 100; + const tree = ( +
+

Hello!

+
Awesome {withStr} fun {withNum}!
+
+ ); + + expect(tree.element).toBe('header'); + expect(tree.attributes).toBeNull(); + + expect(tree.children[0].element).toBe('h1'); + expect(tree.children[0].attributes).toEqual({ name: 'title' }); + expect(tree.children[0].children.length).toBe(1); + expect(tree.children[0].children[0]).toBe('Hello!'); + + expect(tree.children[1].element).toBe('div'); + expect(tree.children[1].attributes).toEqual({ name: 'description' }); + + expect(tree.children[1].children[0]).toBe('Awesome '); + expect(tree.children[1].children[1]).toBe(withStr); + expect(tree.children[1].children[2]).toBe(' '); + + expect(tree.children[1].children[3].element).toBe('b'); + expect(tree.children[1].children[3].attributes).toBeNull(); + expect(tree.children[1].children[3].children[0]).toBe('fun '); + expect(tree.children[1].children[3].children[1]).toBe(withNum); + + expect(tree.children[1].children[4]).toBe('!'); + }); + + test('Should creates node tree with classes and functions', () => { + class MyClass {} + const MyFn = () => 'hello'; + const tree = ( + +

Heading

+ Content +
+ ); + + expect(tree.element).toBe(MyClass); + expect(tree.children[0].element).toBeString(); + expect(tree.children[1].element).toBe(MyFn); + }); +}); diff --git a/src/createElement/index.js b/src/createElement/index.js new file mode 100644 index 0000000..33639a1 --- /dev/null +++ b/src/createElement/index.js @@ -0,0 +1 @@ +export * from './createElement'; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1ea9d43 --- /dev/null +++ b/src/index.js @@ -0,0 +1,3 @@ +export * from './Component'; +export * from './createComponent'; +export * from './render'; diff --git a/src/render/index.js b/src/render/index.js new file mode 100644 index 0000000..dc70a4d --- /dev/null +++ b/src/render/index.js @@ -0,0 +1 @@ +export * from './render'; diff --git a/src/render/render-domelements.test.js b/src/render/render-domelements.test.js new file mode 100644 index 0000000..bb8425e --- /dev/null +++ b/src/render/render-domelements.test.js @@ -0,0 +1,72 @@ +/* eslint-env jest */ +/** @jsx createElement */ + +import { createElement } from '../createElement'; +import { render } from './render'; + +describe('render() DOMElements', () => { + test('Should render DOMElements with "className" prop as "class"', () => { + const parentRoot = document.createElement('div'); + const parentElement =
Content
; + render(parentElement, parentRoot); + + const received = parentRoot.outerHTML; + const expected = '
Content
'; + expect(received).toBe(expected); + }); + + test('Should render DOMElements with "htmlFor" prop as "for"', () => { + const parentRoot = document.createElement('div'); + const parentElement =
Item
; + render(parentElement, parentRoot); + + const received = parentRoot.outerHTML; + const expected = '
Item
'; + expect(received).toBe(expected); + }); + + test('Should render DOMElements with "style" prop as directly style modifications', () => { + const styles = { fontSize: '14px', color: 'blue' }; + const parentRoot = document.createElement('div'); + const parentElement =
Item
; + render(parentElement, parentRoot); + + const received = parentRoot.outerHTML; + const expected = '
Item
'; + expect(received).toBe(expected); + }); + + test('Should render DOMElements with provided events, props matching "/^on[A-Z][A-Za-z]+/"', () => { + const onClick = jest.fn(); + const onFocus = jest.fn(); + const parentRoot = document.createElement('div'); + const parentElement = ; + render(parentElement, parentRoot); + + const button = parentRoot.querySelector('button'); + const clickEvent = new Event('click'); + const focusEvent = new Event('focus'); + button.dispatchEvent(clickEvent); + button.dispatchEvent(focusEvent); + + expect(onClick).toHaveBeenCalledWith(clickEvent); + expect(onFocus).toHaveBeenCalledWith(focusEvent); + }); + + test('Should only render DOMElements strings/numbers and ignore void values', () => { + const parentRoot = document.createElement('div'); + const parentElement =
Hello{true} every{undefined}one{null} {10}!
; + render(parentElement, parentRoot); + + const received = parentRoot.outerHTML; + const expected = '
Hello everyone 10!
'; + expect(received).toBe(expected); + }); + + test('Should throw error if DOMElements have invalid types', () => { + const parentRoot = document.createElement('div'); + const parentElement =
Hello{{}} everyone{/a/}!
; + const fn = () => render(parentElement, parentRoot); + expect(fn).toThrow('Invalid children.'); + }); +}); diff --git a/src/render/render-tree.test.js b/src/render/render-tree.test.js new file mode 100644 index 0000000..f888209 --- /dev/null +++ b/src/render/render-tree.test.js @@ -0,0 +1,94 @@ +/* eslint-env jest */ +/** @jsx createElement */ + +import { Component } from '../Component'; +import { createElement } from '../createElement'; +import { render } from './render'; + +describe('render() tree', () => { + test('Should render basic HTMLElements tree', () => { + const parentElement =
Hello
; + const parentRoot = document.createElement('div'); + render(parentElement, parentRoot); + + const received = parentRoot.outerHTML; + const expected = '
Hello
'; + expect(received).toBe(expected); + }); + + test('Should render nested HTMLElements tree', () => { + const parentRoot = document.createElement('div'); + const parentElement = ( +
+

Hello

+ +
+ ); + render(parentElement, parentRoot); + + const received = parentRoot.outerHTML; + const expected = [ + '
', + '

Hello

', + '', + '
' + ].join(''); + expect(received).toBe(expected); + }); + + test('Should render functions components', () => { + const Title = ({ name, children }) =>

Title: {children}

; + const parentRoot = document.createElement('div'); + const parentElement = ( +
+ Welcome +

Content

+
+ ); + render(parentElement, parentRoot); + + const received = parentRoot.outerHTML; + const expected = '

Title: Welcome

Content

'; + expect(received).toBe(expected); + }); + + test('Should render class components', () => { + class Header extends Component { + render() { + const { name, children } = this.props; + return ( +
+

{children}

+

Best heading ever!

+
+ ); + } + } + + const parentRoot = document.createElement('div'); + const parentElement = ( +
+
Welcome
+

Content

+
+ ); + render(parentElement, parentRoot); + + const received = parentRoot.outerHTML; + const expected = [ + '
', + '
', + '

Welcome

', + '

Best heading ever!

', + '
', + '
' + ].join(''); + expect(received).toBe(expected); + }); +}); diff --git a/src/render/render.js b/src/render/render.js new file mode 100644 index 0000000..23fd9e1 --- /dev/null +++ b/src/render/render.js @@ -0,0 +1,9 @@ +function buildDOM() { + // TODO: + return document.createElement('div'); +} + +export function render(parentElement, parentRoot) { + const domElement = buildDOM(parentElement); + parentRoot.appendChild(domElement); +}