Skip to content

Commit 6d77ba9

Browse files
Implement core functionalities
0 parents  commit 6d77ba9

File tree

13 files changed

+3072
-0
lines changed

13 files changed

+3072
-0
lines changed

.gitignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.DS_Store
2+
.data/
3+
.docs/
4+
.dump/
5+
.idea/
6+
coverage/
7+
journal/
8+
9+
*.db
10+
*.iml
11+
*.log
12+
*.rdb
13+
*.zip
14+
15+
.todo.md

LICENSE

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
MIT License
2+
3+
Copyright © 2020 - Sebastien Filion
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
7+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
8+
persons to whom the Software is furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
11+
Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
14+
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
15+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
16+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
# Functional Component
2+
3+
This is an experiemental project. The goal is to build a rendering framework
4+
leveraging the [web components API](https://developer.mozilla.org/en-US/docs/Web/Web_Components).
5+
If you don't know what to do with it... it's okay.
6+
7+
## API
8+
9+
### `factorizeComponent`
10+
11+
Factorizes a component given a render function, a state and an arbitrary number of composable functions.
12+
13+
The first argument is a render function. The function is called once when the component is connected to the DOM.
14+
The render function should accept two parameters, the first one is the element that is being rendered.
15+
The second parameter is the current state that should be used to render the component.
16+
17+
The second argument to the `factorizeComponent` function is an object that will be used as the inital state.
18+
19+
`State :: Object`
20+
`R :: (DOMElement, State) -> void`
21+
`F :: (((Component, R, State) -> C) -> void, ((DOMElement) -> E) -> void) -> void`
22+
`factorizeComponent :: ((DOMElement, State) -> void, State, [...F]) -> Component`
23+
24+
```js
25+
window.customElements.define(
26+
"iy-demo",
27+
factorizeComponent(
28+
(e, { title }) => {
29+
const h1 = window.document.createElement("h1");
30+
h1.textContent = title;
31+
e.appendChild(e);
32+
},
33+
{ title: "Bluebird" }
34+
)
35+
);
36+
```
37+
38+
#### Higher-order-function
39+
40+
The `factorizeComponent` also accepts an arbitrary amount of functions as arguments.
41+
Those higher-order-functions should accept 2 parameters; the first one is named `factorize`, the second is named
42+
`construct`.
43+
44+
Both HOFs accepts a function that will be called, respectively, at the factory phase, before the component is
45+
initialize, and, at the construction phase, when the component is being instantiated.
46+
47+
The factorize function has three parameters; the first parameter is a Component constructor;
48+
the second parameter is the render function which can be called to queue a render request;
49+
the third parameter is the initial state.
50+
51+
```js
52+
window.customElements.define(
53+
"iy-demo",
54+
factorizeComponent(
55+
render,
56+
state,
57+
(factorize, construct) => {
58+
factorize((Component, render) => {
59+
Object.defineProperty(
60+
Component.prototype,
61+
"forceRender",
62+
{
63+
configurable: false,
64+
enumerable: false,
65+
value() {
66+
render(this);
67+
},
68+
}
69+
);
70+
71+
return Component;
72+
});
73+
construct((element) => {
74+
element.dataset.forceRender = true;
75+
});
76+
}
77+
)
78+
);
79+
```
80+
81+
### `fetchTemplate`
82+
83+
Fetches a HTML file from a server and returns the Promise of a `<template>`.
84+
85+
```js
86+
const element = window.document.querySelector("div");
87+
fetchTemplate("/demo.html")()
88+
.then((template) => {
89+
element.appendChild(template.content.cloneNode(true));
90+
});
91+
```
92+
93+
### `useAttributes`
94+
95+
Creates a reactive lifecycle with a simple state reducer.
96+
When a user or a program sets an attribute of the component, the validation function is called which decides if the
97+
component should be rendered again.
98+
99+
The `useAttributes` function accepts a function to validate an observed attributes value and create a new state. The
100+
validation function should have three parameters. The first one is an object representing the attribute that was
101+
changed, the second is the element that is affected and the last is the current state of the element. The validation
102+
function shoudl return a state fragment or false to cancel the render.
103+
104+
The hook function also takes a map object for all of the attributes to observe.The value is a function to transform
105+
the value before the validation function called. If not transformation is needed, just pass the identity function.
106+
`(x) => x`
107+
108+
```js
109+
window.customElements.define(
110+
"iy-demo",
111+
factorizeComponent(
112+
(element, { value }) => {
113+
const span = element.querySelector("span");
114+
span.textContent = String(value);
115+
},
116+
{ value: 0 },
117+
useAttributes(
118+
({ oldValue, value }) => (oldValue !== value && value >= 0) ? ({ value }) : false,
119+
{
120+
value: Number
121+
}
122+
),
123+
(factorize) => {
124+
factorize((Component) => {
125+
Object.defineProperty(
126+
Component.prototype,
127+
"connectedCallback",
128+
{
129+
enumerable: true,
130+
value() {
131+
console.log("hello");
132+
const span = window.document.createElement("span");
133+
const button = window.document.createElement("button");
134+
button.textContent = "Add";
135+
this.appendChild(span);
136+
this.appendChild(button);
137+
button.addEventListener("click", () => {
138+
const v = this.getAttribute("value");
139+
this.setAttribute("value", String(Number(v) + 1));
140+
});
141+
},
142+
},
143+
);
144+
return Component;
145+
});
146+
}
147+
)
148+
);
149+
```
150+
151+
### `useCallbacks`
152+
153+
Hooks into the component's lifecycle. Learn more about [lifecycle callbacks on MDN](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements#using_the_lifecycle_callbacks)
154+
155+
The function accepts as argument an object of function to hook into one of the following callback:
156+
`connectedCallback`, `disconnectedCallback`, `attributeChangedCallback` and `adoptedCallback`.
157+
158+
Each callback function will be called when appropriate with the element, the relevant options and a `asyncRender`
159+
function as arguments.
160+
The `asyncRender` function can be called at any moment with a "state setter" function. This returns a thunk function
161+
that may accept an argument. When the thunk function is called, the "state setter" function is called with the
162+
current element and state as argument. This function should return a state fragment or false. The state fragment is
163+
then merged with the current state and set the relevant component attributes. See `useAttribute`.
164+
165+
```js
166+
window.customElements.define(
167+
"iy-demo",
168+
factorizeComponent(
169+
(element, { value }) => {
170+
const span = element.querySelector("span");
171+
span.textContent = String(value);
172+
},
173+
{ value: 0 },
174+
useCallbacks({
175+
connectedCallback: (element, render) => {
176+
const span = window.document.createElement("span");
177+
const button = window.document.createElement("button");
178+
button.textContent = "Add";
179+
element.appendChild(span);
180+
element.appendChild(button);
181+
button.addEventListener("click", render((e, { value }) => ({ value: ++value })));
182+
}
183+
}),
184+
(factorize) => {
185+
factorize((Component, render) => {
186+
Object.defineProperty(
187+
Component,
188+
"observedAttributes",
189+
{
190+
enumerable: true,
191+
value: ["value"],
192+
},
193+
);
194+
195+
Object.defineProperty(
196+
Component.prototype,
197+
"attributeChangedCallback",
198+
{
199+
enumerable: true,
200+
value(name, oldValue, value) {
201+
this[StateSymbol][name] = value;
202+
render(this, Object.assign({}, this[StateSymbol]));
203+
},
204+
},
205+
);
206+
207+
return Component;
208+
});
209+
}
210+
)
211+
);
212+
```
213+
214+
### `useShadow`
215+
216+
Attaches a shadow root to every instance of the Component.
217+
218+
```js
219+
window.customElements.define(
220+
"iy-demo",
221+
factorizeComponent(
222+
(element, { value }) => {
223+
const span = element.querySelector("span");
224+
span.textContent = String(value);
225+
},
226+
{ value: 0 },
227+
useShadow({ mode: "open" })
228+
)
229+
);
230+
```
231+
232+
### `useTemplate`
233+
234+
Automatically appends a clone of a template to the element or the element's shadow root.
235+
236+
The function accepts a function that must return a template instance or a Promise of a template instance.
237+
Optionally, the function can also be passed an object as the second argument that is used to define
238+
children that would be often queried during the render phase.
239+
The object's values must be a function that will accept the component instance as the first parameter
240+
and return a child element or node list.
241+
The elements will be accessible as the `elements` property of the Component instance element.
242+
243+
```js
244+
window.customElements.define(
245+
"iy-demo",
246+
factorizeComponent(
247+
(e, { value }) => {
248+
e.elements.number.textContent = String(value);
249+
},
250+
{ value: 0 },
251+
useShadow({ mode: "open" }),
252+
useTemplate(
253+
() => {
254+
const t = window.document.createElement("template");
255+
t.innerHTML = `<span>0</span><button>Add</button>`
256+
257+
return t;
258+
},
259+
{
260+
number: (e) => e.shadowRoot.querySelector("span"),
261+
addButton: (e) => e.shadowRoot.querySelector("button")
262+
}
263+
)
264+
),
265+
);
266+
```
267+

0 commit comments

Comments
 (0)