aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAkshay Nair <phenax5@gmail.com>2023-08-13 14:50:26 +0530
committerAkshay Nair <phenax5@gmail.com>2023-08-13 14:50:26 +0530
commit1f147a779114a641b4abe5e47ac0b05433ec02fb (patch)
treed27845a0ac2deee6bd5ba652da692e733c0cf589
parent48455e6caaa7ad98316bbb4896b59f4487ad8650 (diff)
downloadcss-everything-1f147a779114a641b4abe5e47ac0b05433ec02fb.tar.gz
css-everything-1f147a779114a641b4abe5e47ac0b05433ec02fb.zip
feat: finalizes component system. it fucking works holy shit.
-rw-r--r--TODO.norg9
-rw-r--r--src/declarations.ts102
-rw-r--r--src/index.ts35
-rw-r--r--tests/fixtures/todo-app/index.html49
-rw-r--r--tests/todo-app.spec.ts33
5 files changed, 220 insertions, 8 deletions
diff --git a/TODO.norg b/TODO.norg
index c055580..cd74df4 100644
--- a/TODO.norg
+++ b/TODO.norg
@@ -11,6 +11,15 @@
- (x) `attr` function
- (x) `set-attr` should allow specifying id?
- (x) `set-attr` + remove attribute?
+ - (x) `pair` parsing
+ - (x) `selector` parsing
+ - (x) `map` data structure
+ - (x) component system (with variables. `instance(button#my-btn)`)
+ - ( ) instance access
+ - ( ) More complex selector support for cssx-children
+ - ( ) `add-element` & `remove-element`
+ - ( ) string concatenation
+ - ( ) `request` error handling
- ( ) keyboard events
- ( ) Evaluate `calc`
- ( ) Additional events
diff --git a/src/declarations.ts b/src/declarations.ts
new file mode 100644
index 0000000..6c33fda
--- /dev/null
+++ b/src/declarations.ts
@@ -0,0 +1,102 @@
+import { EvalActions, evalExpr } from './eval'
+import { Expr, Selector, SelectorComp, parseDeclarations } from './parser'
+import { match, matchString } from './utils/adt'
+
+export interface Declaration {
+ selector: Selector
+ properties: Map<string, Expr>
+}
+
+export interface DeclarationEval {
+ selector: Selector
+ properties: Array<readonly [string, string]>
+}
+
+export const evaluateDeclaration = async (
+ { selector, properties }: Declaration,
+ actions: EvalActions,
+): Promise<DeclarationEval> => {
+ if (properties.size === 0) return { selector, properties: [] }
+
+ const props = await Promise.all(
+ [...properties.entries()].map(async ([key, expr]) => {
+ // Ignore errors?
+ const result = await evalExpr(expr, actions).catch(e => console.warn(e))
+ return [key, result ?? ''] as const
+ }),
+ )
+
+ return { selector, properties: props }
+}
+
+const instanceCountMap = new Map<string, number>()
+const getUniqueInstanceId = (id: string) => {
+ const instanceCount = instanceCountMap.get(id) ?? 0
+ instanceCountMap.set(id, instanceCount + 1)
+ return `${id}--index-${instanceCount}`
+}
+
+export const toDeclaration = (expr: Expr): Declaration | undefined => {
+ let selector: Selector | undefined
+ const properties: Map<string, Expr> = new Map()
+ let isInstance = false
+
+ match(expr, {
+ Selector: sel => {
+ selector = sel
+ },
+ Call: ({ name, args }) => {
+ matchString(name, {
+ instance: () => {
+ isInstance = true
+ const [sel, map] = args
+ match(sel, {
+ Selector: sel => {
+ selector = sel
+ },
+ _: _ => {},
+ })
+ match(map, {
+ Call: ({ name, args }) => {
+ if (name !== 'map') return
+ for (const arg of args) {
+ match(arg, {
+ Pair: ({ key, value }) => properties.set(key, value),
+ _: _ => {},
+ })
+ }
+ },
+ })
+ },
+ _: () => {
+ throw new Error(`weird function in cssx-chi9ldren: ${name}`)
+ },
+ })
+ },
+ _: () => {},
+ })
+
+ if (!selector) return undefined
+
+ if (isInstance) {
+ const baseId = selector.id
+ selector.id = getUniqueInstanceId(selector.id)
+ selector.selectors.push(SelectorComp.Attr(['data-instance', baseId]))
+ }
+
+ return { selector, properties }
+}
+
+export const extractDeclaration = async (
+ input: string,
+ actions: EvalActions,
+): Promise<Array<DeclarationEval>> => {
+ const exprs = parseDeclarations(input)
+ const declrs = await Promise.all(
+ exprs
+ .map(toDeclaration)
+ .filter(declr => !!declr)
+ .map(declr => declr && evaluateDeclaration(declr, actions)),
+ )
+ return declrs.filter(declr => !!declr) as Array<DeclarationEval>
+}
diff --git a/src/index.ts b/src/index.ts
index 1c8184a..80e67d9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,7 @@
import { EvalActions, evalExpr } from './eval'
+import { extractDeclaration, DeclarationEval } from './declarations'
import { parse } from './parser'
+import { match } from './utils/adt'
const UNSET_PROPERTY_VALUE = '<unset>'
const EVENT_HANDLERS = {
@@ -36,9 +38,12 @@ export const getPropertyValue = ($element: Element, prop: string) => {
return !value || value === UNSET_PROPERTY_VALUE ? '' : value
}
-export const getChildrenIds = ($element: Element) => {
+export const getDeclarations = (
+ $element: Element,
+ actions: EvalActions,
+): Promise<Array<DeclarationEval>> => {
const value = getPropertyValue($element, '--cssx-children')
- return value.split(/(\s*,\s*)|\s+/g).filter(Boolean)
+ return extractDeclaration(value, actions)
}
const getEvalActions = ($element: Element, event: any): EvalActions => ({
@@ -132,22 +137,36 @@ export const manageElement = async (
const html = getPropertyValue($element, '--cssx-disgustingly-set-innerhtml')
if (html) $element.innerHTML = html.replace(/(^'|")|('|"$)/g, '')
- const childrenIds = getChildrenIds($element)
- if (childrenIds.length > 0) {
+ const declarations = await getDeclarations(
+ $element,
+ getEvalActions($element, null),
+ )
+ if (declarations.length > 0) {
const LAYER_CLASS = 'cssx-layer'
const $childrenRoot =
$element.querySelector(`:scope > .${LAYER_CLASS}`) ??
Object.assign(document.createElement('div'), { className: LAYER_CLASS })
$element.appendChild($childrenRoot)
- for (const childId of childrenIds) {
- const selector = childId.split('#')
- const [tag, id] = selector.length >= 2 ? selector : ['div', ...selector]
+ for (const declaration of declarations) {
+ const { tag, id, selectors } = declaration.selector
+ const tagName = tag || 'div'
+
let $child = $childrenRoot.querySelector(`:scope > #${id}`)
const isNewElement = !$child
if (!$child) {
- $child = Object.assign(document.createElement(tag || 'div'), { id })
+ $child = Object.assign(document.createElement(tagName), { id })
}
+
+ // Add selectors
+ for (const selector of selectors) {
+ match(selector, {
+ ClassName: cls =>
+ !$child?.classList.contains(cls) && $child?.classList.add(cls),
+ Attr: ([key, val]) => $child?.setAttribute(key, val),
+ })
+ }
+
$childrenRoot.appendChild($child)
await manageElement($child, isNewElement)
}
diff --git a/tests/fixtures/todo-app/index.html b/tests/fixtures/todo-app/index.html
new file mode 100644
index 0000000..66a9798
--- /dev/null
+++ b/tests/fixtures/todo-app/index.html
@@ -0,0 +1,49 @@
+<html lang="en">
+ <head>
+ <title>Task destroyer</title>
+ <meta charset="UTF-8" />
+ <style>
+ body {
+ --cssx-children: form#task-input-form #task-list;
+ }
+
+ #task-input-form {
+ --cssx-on-submit:
+ prevent-default()
+ js-eval('console.log("todo: implement add new task")')
+ ;
+
+ --cssx-children:
+ input#text-input[data-testid="add-task-input"]
+ button#create-task-btn[type="submit"][data-testid="add-task-btn"]
+ ;
+ }
+
+ #text-input {}
+ #create-task-btn {
+ --cssx-text: Submit;
+ }
+
+ #task-list {
+ --cssx-children:
+ instance(li#task-item, map(
+ --text: "hello world",
+ --checked: 0
+ ))
+ instance(li#task-item, map(
+ --text: "coolio stuff",
+ --checked: 0
+ ))
+ ;
+ }
+
+ [data-instance="task-item"] {
+ --text: "default text";
+ --checked: 0;
+
+ --cssx-text: var(--text);
+ }
+ </style>
+ </head>
+ <body></body>
+</html>
diff --git a/tests/todo-app.spec.ts b/tests/todo-app.spec.ts
new file mode 100644
index 0000000..1e0cd72
--- /dev/null
+++ b/tests/todo-app.spec.ts
@@ -0,0 +1,33 @@
+import { getByTestId, prettyDOM } from '@testing-library/dom'
+import '@testing-library/jest-dom'
+import { delay, loadHTMLFixture } from './util'
+
+describe('todo-app example', () => {
+ describe('Add new task', () => {
+ beforeAll(async () => {
+ await loadHTMLFixture('todo-app')
+ })
+
+ it('should add new unchecked task', async () => {
+ const $textInput = getByTestId<HTMLInputElement>(
+ document.body,
+ 'add-task-input',
+ )
+ $textInput.value = 'Buy Milk'
+
+ const $addBtn = getByTestId<HTMLButtonElement>(
+ document.body,
+ 'add-task-btn',
+ )
+ $addBtn.click()
+
+ await delay(100)
+
+ console.log(prettyDOM(document.body))
+ console.log()
+ console.log()
+ console.log()
+ console.log()
+ })
+ })
+})