The vanilla alternative for writing JSX-based React applications
git clone git://soophie.de/figure
4
### Example6
```javascript7
import {figure, html, css} from '...';7
import figure from '...';9
figure({ createElement });9
const { html, dyn } = figure({ createElement });11
const Counter = () => {12
const [count, setCount] = useState(0);11
const global = createContext({});14
const style = css`15
.counter { ... }16
`;13
function App() {14
const [name, setName] = useState('world');16
const context = {};18
return html`19
<div class="counter">20
${style}21
<button @click="${() => setCount(count - 1)}">-</button>22
<p>${count}</p>23
<button @click="${() => setCount(count + 1)}">+</button>24
</div>19
${dyn(global.Provider, { value: context }), html`20
<h1>Hello ${name}!</h1>21
<p>Some description text.</p>22
`}23
`;24
}28
render(createElement(Counter), document.querySelector('#app'));26
render(createElement(App), document.querySelector('#root'));27
```
1
<div id="app"></div>3
<script type="module">5
import {useState, createElement} from 'https://cdn.skypack.dev/react';6
import {render} from 'https://cdn.skypack.dev/react-dom';7
import {figure, html, css} from '../lib.js';9
const Counter = () => {10
const [count, setCount] = useState(0);12
const style = css`13
.counter {14
display: inline-flex;15
align-items: center;16
gap: 10px;17
padding: 50px;18
}20
.counter p {21
width: 30px;22
margin: 0px;23
text-align: center;24
font-family: monospace;25
font-size: 16px;26
}28
.counter button {29
width: 32px;30
height: 32px;31
cursor: pointer;32
}33
`;35
return html`36
<div class="counter">37
${style}38
<button @click="${() => setCount(count - 1)}">-</button>39
<p>${count}</p>40
<button @click="${() => setCount(count + 1)}">+</button>41
</div>42
`;43
}45
const app = document.querySelector('#app');46
figure({ createElement });47
render(createElement(Counter), app);49
</script>
1
<div id="app"></div>3
<script type="module">5
import {useState, createElement} from 'https://cdn.skypack.dev/react';6
import {render} from 'https://cdn.skypack.dev/react-dom';7
import {figure, html, css} from '../lib.js';9
const Range = ({index, value, setValue}) => {10
const style = css`11
.range input[type=range] {12
width: 100%;13
-webkit-appearance: none;14
}16
.range.v-${index}-${value} input[type=range]::-webkit-slider-runnable-track {17
background-color: rgb(${Array.from([0, 0, 0], (item, i) => i === index ? value : item).join(', ')});18
width: 100%;19
height: 12px;20
}22
.range input[type=range]::-webkit-slider-thumb {23
background-color: #CCC;24
width: 12px;25
height: 12px;26
-webkit-appearance: none;27
cursor: pointer;28
}29
`;31
return html`32
<div class="range v-${index}-${value}">33
${style}34
<input35
type="range"36
@change="${(event) => setValue(Number(event.target.value))}"37
min="0"38
max="255"39
value="${value}"40
>41
</div>42
`;43
}45
const Picker = () => {46
const [red, setRed] = useState(255);47
const [green, setGreen] = useState(65);48
const [blue, setBlue] = useState(155);50
const style = css`51
.picker {52
display: inline-flex;53
flex-direction: column;54
gap: 10px;55
padding: 50px;56
}58
.picker .preview {59
width: 200px;60
height: 200px;61
background-color: rgb(${red}, ${green}, ${blue});62
}64
.picker p {65
margin: 0px;66
text-align: center;67
font-family: monospace;68
font-size: 16px;69
}70
`;72
const hex = (dec) => dec.toString(16).toUpperCase().padStart(2, '0');74
return html`75
<div class="picker">76
${style}77
<div class="preview"></div>78
<p>rgb(${red}, ${green}, ${blue})</p>79
<p>#${hex(red)}${hex(green)}${hex(blue)}</p>80
${Range({ index: 0, value: red, setValue: setRed })}81
${Range({ index: 1, value: green, setValue: setGreen })}82
${Range({ index: 2, value: blue, setValue: setBlue })}83
</div>84
`;85
}87
const app = document.querySelector('#app');88
figure({ createElement });89
render(createElement(Picker), app);91
</script>
1
import { CreateElement, Option, Options, Props, ReactElement, ReactFunction, Refs, Slices, Values } from './types.ts';1
import { Options, Props, ReactElement, ReactFunction, Refs, Slices, Values } from './types.ts';3
const parser = new DOMParser();4
let count = 1000;5
let create: Option<CreateElement> = null;3
export default function figure({ createElement }: Options) {7
function figure({ createElement }: Options): void {8
create = createElement;9
}11
function html(slices: Slices, ...values: Values): ReactElement {12
const elements = parse(slices, ...values);13
if (elements.length === 0) {14
throw 'No DOM element was returned!';15
}16
if (elements.length > 1) {17
console.warn('Only one DOM element can be returned!');18
}19
return elements[0];20
}5
// the parser for interpreting HTML6
const parser = new DOMParser();7
// the counter for creating unique references8
let count = 0;22
function parse(slices: Slices, ...values: Values): ReactElement[] {23
const [html, refs] = compose(slices, values);24
let dom;25
try {26
dom = parser.parseFromString(html, 'text/html');27
}28
catch (error) {29
console.error(error);30
throw 'Invalid DOM structure!';10
/**11
* Converts the template literal HTML syntax into React elements.12
* @param {Slices} slices - The template literal slices13
* @param {Values} values - The template literal values14
* @return {ReactElement[]} The converted HTML as React elements.15
*/16
function html(slices: Slices, ...values: Values): ReactElement[] {17
const [html, refs] = compose(slices, values);18
let dom;19
try {20
dom = parser.parseFromString(html, 'text/html');21
}22
catch (error) {23
console.error(error);24
throw 'Invalid DOM structure!';25
}26
// collect all nodes from head and body27
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];28
return nodes.map((node) => render(node, refs));29
}32
const nodes = [...dom.head.childNodes, ...dom.body.childNodes];33
return nodes.map((node) => render(node, refs));34
}36
/**37
* Joins the template literal slices together and replaces the values with references.38
* The values are being mapped to there corresponding references and with the populated39
* HTML string returned.40
*41
* @param {string[]} slices - The template literal slices42
* @param {any[]} values - The template literal values43
* @return {any[]} The joined HTML string and the values mapped to there references.44
*/45
function compose(slices: Slices, values: Values): [string, Refs] {46
if (slices == null) {47
// handles dyn function without body48
return ['', {}];49
}50
const refs: Refs = {};51
let slice = '';52
for (let i = 0; i < slices.length; i++) {53
slice += slices[i];54
if (values[i] != null) {55
const uid = `$seg-${count++}`;56
refs[uid] = values[i];57
slice += uid ?? '';31
/**32
* Joins the template literal slices together and replaces the values with references.33
* The values are being mapped to there corresponding references and with the populated34
* HTML string returned.35
*36
* @param {Slices} slices - The template literal slices37
* @param {Values} values - The template literal values38
* @return {[string, Refs]} The joined HTML string and the values mapped to there references.39
*/40
function compose(slices: Slices, values: Values): [string, Refs] {41
if (slices == null) {42
// handle dyn function without body43
return ['', {}];44
}45
const refs: Refs = {};46
let slice = '';47
for (let i = 0; i < slices.length; i++) {48
slice += slices[i];49
if (values[i] != null) {50
const uid = `$fig-${count++}`;51
refs[uid] = values[i];52
slice += uid ?? '';53
}54
}55
return [slice.trim(), refs];56
}60
return [slice.trim(), refs];61
}63
/**64
* Injects the values into the corresponding reference locations of the string.65
*66
* @param {string} slice - The string containing references67
* @param {any[]} refs - The values mapped to there references68
* @return {any[]} The string populated with the passed values69
*/70
function feed(slice: string, refs: Refs): ReactElement[] {71
const expr = /\$seg-\d+/g;72
const elements: ReactElement[] = [];73
let match = null;74
let last = 0;75
while ((match = expr.exec(slice)) !== null) {76
const index = match.index;77
const uid = match[0];78
elements.push(slice.substring(last, index));79
let value = refs[uid];80
if (value instanceof Function) {81
value = value();58
/**59
* Injects the values into the corresponding reference locations of the string.60
*61
* @param {string} slice - The string containing references62
* @param {Refs} refs - The values mapped to there references63
* @return {ReactElement[]} The string populated with the passed values64
*/65
function feed(slice: string, refs: Refs): ReactElement[] {66
const expr = /\$fig-\d+/g;67
const elements: ReactElement[] = [];68
let match = null;69
let last = 0;70
while ((match = expr.exec(slice)) !== null) {71
const index = match.index;72
const uid = match[0];73
elements.push(slice.substring(last, index));74
let value = refs[uid];75
if (value instanceof Function) {76
value = value();77
}78
elements.push(value);79
last = index + uid.length;80
}83
elements.push(value);84
last = index + uid.length;81
elements.push(slice.substring(last));82
return elements;83
}86
elements.push(slice.substring(last));87
return elements;88
}90
function dyn(element: ReactFunction, props: Props): (slices: Slices, ...values: Values) => ReactElement {91
return (slices: Slices, ...values: Values) => {92
if (create == null) {93
throw 'Invalid state! Figure was not initialized!';85
/**86
* Converts a HTML node into a React element.87
*88
* @param {Node} node - The HTML node89
* @param {Refs} refs - The values mapped to there references90
* @return {ReactElement[]} The converted HTML node as React element91
*/92
function render(node: Node, refs: Refs): ReactElement[] {93
if (node.nodeType === Node.TEXT_NODE) {94
const text = node as Text;95
if (text.textContent == null) {96
// ignore empty text nodes97
return [];98
}99
return feed(text.textContent, refs);100
}95
return create(element, props, ...parse(slices, ...values));96
};97
}99
function render(node: Node, refs: Refs): ReactElement[] {100
if (node.nodeType === Node.TEXT_NODE) {101
const text = node as Text;102
if (text.textContent == null) {101
if (node.nodeType === Node.COMMENT_NODE) {102
// ignore comments103
return [];104
}105
return feed(text.textContent, refs);106
}107
if (node.nodeType === Node.COMMENT_NODE) {108
return [];109
}110
const element = node as HTMLElement;111
const tag = element.tagName;112
const attributes: Props = {};113
for (const attribute of element.attributes) {114
const key = attribute.name;115
const slice = attribute.textContent;116
if (slice == null) {117
continue;118
}119
let isDynamic = false;120
for (const ref in refs) {121
if (slice === ref) {122
let match = null;123
if ((match = /^@(\w+)$/.exec(key)) !== null) {124
const event = match[1];125
attributes[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref];126
}127
else if ((match = /^\[(\w+)\]$/.exec(key)) !== null) {128
const property = match[1];129
attributes[property] = refs[ref];130
}131
else {132
attributes[key] = refs[ref];133
}134
isDynamic = true;135
break;105
const element = node as HTMLElement;106
const tag = element.tagName;107
const props: Props = {};108
// iterate over each attribute and add it to the props109
for (const attribute of element.attributes) {110
const key = attribute.name;111
const slice = attribute.textContent;112
if (slice == null) {113
// ignore empty attribute values114
continue;115
}137
}138
if (!isDynamic) {139
if (key === 'style') {140
const styles: Record<string, string> = {};141
for (const item of slice.split(';')) {142
if (item.trim().length === 0) {143
break;144
}145
let [key, value] = item.split(':');146
key = key.trim();116
let isDynamic = false;117
for (const ref in refs) {118
if (slice === ref) {119
let match = null;148
if ((match = /-(\w)/.exec(key)) !== null) {149
const char = match[1];150
key = key.replace(`-${char}`, char.toUpperCase());120
if ((match = /^on:(\w+)$/.exec(key)) !== null) {121
// add event to props122
const event = match[1];123
props[`on${event.substring(0, 1).toUpperCase()}${event.substring(1)}`] = refs[ref];124
}125
else {126
// add attribute to props127
props[key] = refs[ref];128
}152
value = value.trim();153
styles[key] = feed(value, refs).join('');129
isDynamic = true;130
break;131
}155
attributes[key] = styles;132
}157
else {158
attributes[key] = feed(slice, refs).join('');133
if (!isDynamic) {134
if (key === 'style') {135
// convert style attribute into object format136
const styles: Record<string, string> = {};137
for (const item of slice.split(';')) {138
if (item.trim().length === 0) {139
break;140
}141
let [key, value] = item.split(':');142
key = key.trim();143
let match = null;144
if ((match = /-(\w)/.exec(key)) !== null) {145
const char = match[1];146
key = key.replace(`-${char}`, char.toUpperCase());147
}148
value = value.trim();149
styles[key] = feed(value, refs).join('');150
}151
props[key] = styles;152
}153
else {154
props[key] = feed(slice, refs).join('');155
}156
}157
}158
const children: ReactElement[] = [];159
// recursively render all child nodes160
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));161
return [createElement(tag, props, ...children)];162
}162
const children: ReactElement[] = [];163
(node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));164
if (create == null) {165
throw 'Invalid state! Figure was not initialized!';166
}167
return [create(tag, attributes, ...children)];168
}170
function css(slices: Slices, ...values: Values): ReactElement {171
const [css, refs] = compose(slices, values);172
return html`173
<style type="text/css">174
${feed(css, refs).join('')}175
</style>176
`;177
}179
function style(slices: Slices, ...values: Values): Record<string, string> {180
const [css, refs] = compose(slices, values);181
const styles: Record<string, string> = {};182
for (const item of css.split(';')) {183
if (item.trim().length === 0) {184
break;185
}186
let [key, value] = item.split(':');187
key = key.trim();188
let match = null;189
if ((match = /-(\w)/.exec(key)) !== null) {190
const char = match[1];191
key = key.replace(`-${char}`, char.toUpperCase());192
}193
value = value.trim();194
styles[key] = feed(value, refs).join('');164
/**165
* Creates a dynamic component with its own state.166
* Should be used if the component is statefull (contains hooks).167
*168
* @param {ReactFunction} element - The string containing references169
* @param {Props} props - The component properties170
* @param {ReactElement[]} children - The child elements171
* @return {ReactElement} The created React component172
*/173
function dyn(element: ReactFunction, props?: Props, children?: ReactElement[]): ReactElement {174
return createElement(element, props, children);175
}196
return styles;197
}199
export { figure, html, dyn, css, style };177
return { html, dyn };178
}
1
export type Option<T> = T | null | undefined;1
export type ReactElement = unknown;2
export type ReactFunction = unknown;3
export type CreateElement = (element: ReactFunction, props?: Props, ...children: ReactElement[]) => ReactElement;