commit 71bcd6376c673c2b386604e00d1e55a030d892b3
parent f07d629d85be64a3a4ce2166b5d45b5b7715d0f1
Author: typable <contact@typable.dev>
Date: Fri, 16 Sep 2022 23:55:51 +0200
Added picker example
Diffstat:
D | example.html | | | 105 | ------------------------------------------------------------------------------- |
A | examples/picker.html | | | 92 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
D | lib.js | | | 123 | ------------------------------------------------------------------------------- |
A | tlx.js | | | 138 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 230 insertions(+), 228 deletions(-)
diff --git a/example.html b/example.html
@@ -1,105 +0,0 @@
-<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&family=Noto+Sans">
-
-<div id="app"></div>
-
-<style>
-
- * {
- font-family: 'Noto Sans', sans-serif;
- }
-
- body {
- margin: 0;
- }
-
-</style>
-
-<script type="module">
-
- import {useState, createElement} from 'https://cdn.skypack.dev/react';
- import {render} from 'https://cdn.skypack.dev/react-dom';
- import {createTlx, css} from './lib.js';
-
- const tlx = createTlx(createElement);
-
- const Button = (props, $slot) => {
- const styles = {
- ...css`
- min-width: 40px;
- height: 40px;
- border: none;
- outline: none;
- border: 1px solid #a3a3a3;
- background-color: ${props.disabled ? '#f5f5f5' : '#e5e5e5'};
- cursor: ${props.disabled ? 'default' : 'pointer'};
- font-size: 1rem;
- `,
- ...props.styles
- };
- return tlx`
- <button
- disabled="${props.disabled}"
- style="${styles}"
- @click="${() => props.onClick()}"
- >
- ${$slot}
- </button>
- `;
- }
-
- const Counter = (props) => {
- const [defaultValue, setDefaultValue] = useState(0);
- const { min = 0, max = 999, value = defaultValue, setValue = setDefaultValue } = props ?? {};
- const validate = (value) => {
- if(/^\d+$/.test(value) && parseInt(value) <= max) {
- setValue(parseInt(value));
- }
- }
- return tlx`
- <div style="display: inline-flex; width: 100%; border: 1px solid #a3a3a3; border-radius: 4px; overflow: hidden; height: 40px; box-sizing: border-box;">
- ${Button({
- onClick: () => setValue(value - 1),
- styles: css`
- font-size: 1.2rem;
- border-width: 0px 1px 0px 0px;
- `,
- disabled: value <= min
- }, '-')}
- <input type="text" @input="${(event) => validate(event.target.value)}" value="${value}" style="font-size: 1rem; text-align: center; border: none; flex: 1; min-width: 0; height: 40px; outline: none;">
- ${Button({
- onClick: () => setValue(value + 1),
- styles: css`
- font-size: 1.2rem;
- border-width: 0px 0px 0px 1px;
- `,
- disabled: value >= max
- }, '+')}
- </div>
- `;
- }
-
- const App = () => {
- const [quantity, setQuantity] = useState(1);
- const isDisabled = quantity <= 0;
- return tlx`
- <div style="margin: 100px; display: flex; width: 150px; flex-direction: column; gap: 15px; align-items: flex-start;">
- ${Counter({ value: quantity, setValue: setQuantity })}
- ${Button({
- styles: css`
- background-color: ${isDisabled ? '#f5f5f5' : '#f97316'};
- border-color: ${isDisabled ? '#d4d4d4' : '#c2410c'};
- color: ${isDisabled ? '#a3a3a3' : '#ffffff'};
- border-radius: 4px;
- width: 100%;
- `,
- onClick: () => alert(`Added ${quantity} ${quantity > 1 ? 'items' : 'item'} to the cart`),
- disabled: isDisabled
- }, 'Add to cart')}
- </div>
- `;
- }
-
- const app = document.querySelector('#app');
- render(createElement(App), app);
-
-</script>
diff --git a/examples/picker.html b/examples/picker.html
@@ -0,0 +1,92 @@
+<div id="app"></div>
+
+<script type="module">
+
+ import {useState, createElement} from 'https://cdn.skypack.dev/react';
+ import {render} from 'https://cdn.skypack.dev/react-dom';
+ import {createTlx, css} from '../tlx.js';
+
+ const tlx = createTlx(createElement);
+
+ const Range = ({index, value, setValue}) => {
+ const style = css`
+ .range input[type=range] {
+ width: 100%;
+ -webkit-appearance: none;
+ }
+
+ .range.v-${index}-${value} input[type=range]::-webkit-slider-runnable-track {
+ background-color: rgb(${Array.from([0, 0, 0], (item, i) => i === index ? value : item).join(', ')});
+ width: 100%;
+ height: 12px;
+ }
+
+ .range input[type=range]::-webkit-slider-thumb {
+ background-color: #CCC;
+ width: 12px;
+ height: 12px;
+ -webkit-appearance: none;
+ cursor: pointer;
+ }
+ `;
+
+ return tlx`
+ <div class="range v-${index}-${value}">
+ ${style}
+ <input
+ type="range"
+ @change="${(event) => setValue(Number(event.target.value))}"
+ min="0"
+ max="255"
+ value="${value}"
+ >
+ </div>
+ `;
+ }
+
+ const Picker = () => {
+ const [red, setRed] = useState(255);
+ const [green, setGreen] = useState(65);
+ const [blue, setBlue] = useState(155);
+
+ const style = css`
+ .picker {
+ display: inline-flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 50px;
+ }
+
+ .picker .preview {
+ width: 200px;
+ height: 200px;
+ background-color: rgb(${red}, ${green}, ${blue});
+ }
+
+ .picker p {
+ margin: 0px;
+ text-align: center;
+ font-family: monospace;
+ font-size: 16px;
+ }
+ `;
+
+ const hex = (dec) => dec.toString(16).toUpperCase().padStart(2, '0');
+
+ return tlx`
+ <div class="picker">
+ ${style}
+ <div class="preview"></div>
+ <p>rgb(${red}, ${green}, ${blue})</p>
+ <p>#${hex(red)}${hex(green)}${hex(blue)}</p>
+ ${Range({ index: 0, value: red, setValue: setRed })}
+ ${Range({ index: 1, value: green, setValue: setGreen })}
+ ${Range({ index: 2, value: blue, setValue: setBlue })}
+ </div>
+ `;
+ }
+
+ const app = document.querySelector('#app');
+ render(createElement(Picker), app);
+
+</script>
diff --git a/lib.js b/lib.js
@@ -1,123 +0,0 @@
-const parser = new DOMParser();
-let counter = 1000;
-
-export const createTlx = (createElement) => {
- function tlx(parts, ...props) {
- const [html, refs] = ltr(parts, props);
- const dom = parser.parseFromString(html, 'text/html');
- if(dom.body.childNodes.length !== 1) {
- throw 'invalid DOM structure!';
- }
- const node = dom.body.childNodes[0];
- const elements = render(node, refs);
- if(elements.length !== 1) {
- throw 'invalid VDOM structure!';
- }
- return elements[0];
- }
-
- function render(node, refs) {
- if(node.nodeType === Node.TEXT_NODE) {
- return apply(node.textContent, refs);
- }
- const tag = node.tagName.toLowerCase();
- const attributes = {};
- for(const attribute of node.attributes) {
- const key = attribute.name;
- const value = attribute.textContent;
- let isDynamic = false;
- for(const ref in refs) {
- if(value === ref) {
- let match = null;
- if((match = /^@(\w+)$/.exec(key)) !== null) {
- const event = match[1];
- attributes[`on${event.substr(0, 1).toUpperCase()}${event.substr(1)}`] = refs[ref];
- }
- else {
- attributes[key] = refs[ref];
- }
- isDynamic = true;
- break;
- }
- }
- if(!isDynamic) {
- if(key === 'style') {
- const styles = {};
- for(const item of value.split(';')) {
- if(item.trim().length === 0) {
- break;
- }
- let [key, value] = item.split(':');
- key = key.trim();
- let match = null;
- if((match = /-(\w)/.exec(key)) !== null) {
- const char = match[1];
- key = key.replace(`-${char}`, char.toUpperCase());
- }
- value = value.trim();
- styles[key] = apply(value, refs).join('');
- }
- attributes[key] = styles;
- }
- else {
- attributes[key] = apply(value, refs).join('');
- }
- }
- }
- const children = [];
- (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));
- return [createElement(tag, attributes, ...children)];
- }
-
- return tlx;
-}
-
-export const css = (parts, ...props) => {
- const [css, refs] = ltr(parts, props);
- const styles = {};
- for(const item of css.split(';')) {
- if(item.trim().length === 0) {
- break;
- }
- let [key, value] = item.split(':');
- key = key.trim();
- let match = null;
- if((match = /-(\w)/.exec(key)) !== null) {
- const char = match[1];
- key = key.replace(`-${char}`, char.toUpperCase());
- }
- value = value.trim();
- styles[key] = apply(value, refs).join('');
- }
- return styles;
-}
-
-function apply(value, refs) {
- let values = [];
- const expr = /\$tlx-\d+/g;
- let match = null;
- let last = 0;
- while((match = expr.exec(value)) !== null) {
- const index = match.index;
- values.push(value.substring(last, index));
- values.push(refs[match[0]]);
- last = index + match[0].length;
- }
- values.push(value.substring(last));
- return values;
-}
-
-function ltr(parts, props) {
- let string = '';
- const refs = {};
- for(let i = 0; i < parts.length; i++) {
- string += parts[i];
- if(props[i] !== undefined) {
- const id = `$tlx-${counter}`;
- refs[id] = props[i];
- string += id ?? '';
- counter++;
- }
- }
- return [string.trim(), refs];
-}
diff --git a/tlx.js b/tlx.js
@@ -0,0 +1,138 @@
+const parser = new DOMParser();
+let counter = 1000;
+let tlx = null;
+
+export const createTlx = (createElement) => {
+ function parse(parts, ...props) {
+ const [html, refs] = ltr(parts, props);
+ const dom = parser.parseFromString(html, 'text/html');
+ const node = dom.body.childNodes[0] ?? dom.head.childNodes[0];
+ const elements = render(node, refs);
+ if(elements.length !== 1) {
+ throw 'invalid VDOM structure!';
+ }
+ return elements[0];
+ }
+
+ function render(node, refs) {
+ if(node.nodeType === Node.TEXT_NODE) {
+ return apply(node.textContent, refs);
+ }
+ if(node.nodeType === Node.COMMENT_NODE) {
+ return [];
+ }
+ const tag = node.tagName;
+ const attributes = {};
+ for(const attribute of node.attributes) {
+ const key = attribute.name;
+ const value = attribute.textContent;
+ let isDynamic = false;
+ for(const ref in refs) {
+ if(value === ref) {
+ let match = null;
+ if((match = /^@(\w+)$/.exec(key)) !== null) {
+ const event = match[1];
+ attributes[`on${event.substr(0, 1).toUpperCase()}${event.substr(1)}`] = refs[ref];
+ }
+ else {
+ attributes[key] = refs[ref];
+ }
+ isDynamic = true;
+ break;
+ }
+ }
+ if(!isDynamic) {
+ if(key === 'style') {
+ const styles = {};
+ for(const item of value.split(';')) {
+ if(item.trim().length === 0) {
+ break;
+ }
+ let [key, value] = item.split(':');
+ key = key.trim();
+ let match = null;
+ if((match = /-(\w)/.exec(key)) !== null) {
+ const char = match[1];
+ key = key.replace(`-${char}`, char.toUpperCase());
+ }
+ value = value.trim();
+ styles[key] = apply(value, refs).join('');
+ }
+ attributes[key] = styles;
+ }
+ else {
+ attributes[key] = apply(value, refs).join('');
+ }
+ }
+ }
+ const children = [];
+ (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs)));
+ return [createElement(tag, attributes, ...children)];
+ }
+
+ tlx = parse;
+ return tlx;
+}
+
+export const css = (parts, ...props) => {
+ const [css, refs] = ltr(parts, props);
+ return tlx`
+ <style type="text/css">
+ ${apply(css, refs).join('')}
+ </style>
+ `;
+}
+
+export const style = (parts, ...props) => {
+ const [css, refs] = ltr(parts, props);
+ const styles = {};
+ for(const item of css.split(';')) {
+ if(item.trim().length === 0) {
+ break;
+ }
+ let [key, value] = item.split(':');
+ key = key.trim();
+ let match = null;
+ if((match = /-(\w)/.exec(key)) !== null) {
+ const char = match[1];
+ key = key.replace(`-${char}`, char.toUpperCase());
+ }
+ value = value.trim();
+ styles[key] = apply(value, refs).join('');
+ }
+ return styles;
+}
+
+function apply(value, refs) {
+ let values = [];
+ const expr = /\$tlx-\d+/g;
+ let match = null;
+ let last = 0;
+ while((match = expr.exec(value)) !== null) {
+ const index = match.index;
+ values.push(value.substring(last, index));
+ let refValue = refs[match[0]];
+ if(refValue instanceof Function) {
+ refValue = refValue();
+ }
+ values.push(refValue);
+ last = index + match[0].length;
+ }
+ values.push(value.substring(last));
+ return values;
+}
+
+function ltr(parts, props) {
+ let string = '';
+ const refs = {};
+ for(let i = 0; i < parts.length; i++) {
+ string += parts[i];
+ if(props[i] !== undefined) {
+ const id = `$tlx-${counter}`;
+ refs[id] = props[i];
+ string += id ?? '';
+ counter++;
+ }
+ }
+ return [string.trim(), refs];
+}