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 = (
+
+ );
+
+ 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 = (
+
+ );
+
+ 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 = (
+
+ );
+ render(parentElement, parentRoot);
+
+ const received = parentRoot.outerHTML;
+ const expected = [
+ ''
+ ].join('');
+ expect(received).toBe(expected);
+ });
+
+ test('Should render functions components', () => {
+ const Title = ({ name, children }) => Title: {children}
;
+ const parentRoot = document.createElement('div');
+ const parentElement = (
+
+ );
+ render(parentElement, parentRoot);
+
+ const received = parentRoot.outerHTML;
+ const expected = '';
+ 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 = (
+
+ );
+ 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);
+}