From 67748db74f73343b054ee0af1763e376a5470416 Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Fri, 11 Aug 2023 17:13:33 +0530 Subject: feat: adds more functions for eval --- TODO.norg | 9 ++++---- examples/api/content.css | 26 ++++++++++++++++++++++ examples/api/style.css | 10 ++++----- src/eval.ts | 58 +++++++++++++++++++++++++++++++++--------------- src/index.ts | 32 +++++++++++++++++++++++--- tests/eval.spec.ts | 31 +++++++++++++++++++++++--- 6 files changed, 132 insertions(+), 34 deletions(-) create mode 100644 examples/api/content.css diff --git a/TODO.norg b/TODO.norg index 6469cb9..6ec8df2 100644 --- a/TODO.norg +++ b/TODO.norg @@ -1,9 +1,10 @@ * Tasks - (x) Hydrate existing elements instead of re-creating - - ( ) `load-cssx` functions + - (x) `load-cssx` functions + - (x) `get-variable` + - (x) `update-variable` - ( ) Use css units for `delay` function - - ( ) `get-variable` - - ( ) `update-variable` + - ( ) Improve error messages + - ( ) Specify node type + attributes - ( ) Evaluate `calc`? - ( ) `list` & `tuple` data structures? - - ( ) Non div elements diff --git a/examples/api/content.css b/examples/api/content.css new file mode 100644 index 0000000..5463ba2 --- /dev/null +++ b/examples/api/content.css @@ -0,0 +1,26 @@ + +#output-container-content { + padding: 0.5rem; + margin-top: 1rem; + border: 1px solid #888; + + --cssx-children: counter-text btn-increment; + --count: '0'; +} +#output-container-content::after { + content: "Loaded from external stylesheet"; +} + +#counter-text {} +#counter-text::after { content: var(--count) } + +#btn-increment { + display: inline-block; + border: 1px solid gray; + padding: 0.3rem 0.6rem; + font-size: 0.9rem; + cursor: pointer; + + --cssx-on-click: update(output-container-content, --count, '99'); +} +#btn-increment::after { content: "Increment" } diff --git a/examples/api/style.css b/examples/api/style.css index 213d9e2..81eada1 100644 --- a/examples/api/style.css +++ b/examples/api/style.css @@ -4,17 +4,18 @@ body { #load-btn { display: inline-block; - border: 1px solid gray; + background: #5180e9; + color: #000; padding: 0.5rem 1rem; cursor: pointer; --cssx-on-click: add-class(output-container, 'loading') add-class(load-btn, 'loading') - delay('2000') + load-cssx(output-container-content, './content.css') remove-class(output-container, 'loading') - add-class(output-container, 'loaded') remove-class(load-btn, 'loading') + delay('50') js-eval('alert("Loaded page")') ; } @@ -30,7 +31,4 @@ body { #output-container.loading::after { content: "Loading..."; } -#output-container.loaded::after { - content: "This content is loaded my guy"; -} diff --git a/src/eval.ts b/src/eval.ts index 76eaa21..03391c2 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -1,46 +1,67 @@ import { Expr } from "./parser"; import { match, matchString } from "./utils/adt"; -export type Dependencies = { +export type EvalActions = { addClass(id: string, classes: string): Promise removeClass(id: string, classes: string): Promise delay(num: number): Promise jsEval(js: string): Promise - // loadCssx(id: string, url: string): Promise - // getVarable(name: string, def?: string): Promise - // updateVariable(id: string, varName: string, value: string): Promise + loadCssx(id: string, url: string): Promise + getVariable(name: string): Promise + updateVariable(id: string, varName: string, value: string): Promise // calculate ?? } -export const evalExpr = async (expr: Expr, deps: Dependencies): Promise => - match, Expr>(expr, { +type EvalValue = string | undefined | void + +export const evalExpr = async (expr: Expr, actions: EvalActions): Promise => + match, Expr>(expr, { Call: async ({ name, args }) => { - await matchString(name, { + return matchString, string>(name, { 'add-class': async () => { - const id = await evalExpr(args[0], deps) - const classes = await evalExpr(args[1], deps) + const id = await evalExpr(args[0], actions) + const classes = await evalExpr(args[1], actions) if (id && classes) { - await deps.addClass(id, classes) + await actions.addClass(id, classes) } }, 'remove-class': async () => { - const id = await evalExpr(args[0], deps) - const classes = await evalExpr(args[1], deps) + const id = await evalExpr(args[0], actions) + const classes = await evalExpr(args[1], actions) if (id && classes) { - await deps.removeClass(id, classes) + await actions.removeClass(id, classes) } }, 'delay': async () => { - const num = await evalExpr(args[0], deps) - num && await deps.delay(parseInt(num, 10)) + const num = await evalExpr(args[0], actions) + num && await actions.delay(parseInt(num, 10)) }, 'js-eval': async () => { - const js = await evalExpr(args[0], deps) - js && await deps.jsEval(js) + const js = await evalExpr(args[0], actions) + js && await actions.jsEval(js) + }, + 'load-cssx': async () => { + const id = await evalExpr(args[0], actions) + const url = await evalExpr(args[1], actions) + if (id && url) { + await actions.loadCssx(id, url) + } + }, + 'var': async () => { + const varName = await evalExpr(args[0], actions) + const defaultValue = await evalExpr(args[1], actions) + return varName && (actions.getVariable(varName) ?? defaultValue) + }, + 'update': async () => { + const id = await evalExpr(args[0], actions) + const varName = await evalExpr(args[1], actions) + const value = await evalExpr(args[2], actions) + if (id && varName && value) { + actions.updateVariable(id, varName, value) + } }, _: () => Promise.reject(new Error('not supposed to be here')), }) - return undefined }, LiteralString: async s => s, Identifier: async s => s, @@ -48,3 +69,4 @@ export const evalExpr = async (expr: Expr, deps: Dependencies): Promise undefined, }) + diff --git a/src/index.ts b/src/index.ts index 4987e65..9e9443c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { Dependencies, evalExpr } from './eval' +import { EvalActions, evalExpr } from './eval' import { parse } from './parser' const UNSET_PROPERTY_VALUE = '' @@ -33,12 +33,37 @@ const getChildrenIds = ($element: Element) => { return value.split(/(\s*,\s*)|\s+/g).filter(Boolean) } -const evalDeps = (_el: Element): Dependencies => ({ +const getEvalActions = ($element: Element): EvalActions => ({ addClass: async (id, cls) => document.getElementById(id)?.classList.add(cls), removeClass: async (id, cls) => document.getElementById(id)?.classList.remove(cls), delay: (delay) => new Promise((res) => setTimeout(res, delay)), jsEval: async (js) => (0, eval)(js), + loadCssx: async (id, url) => new Promise((resolve, reject) => { + const $link = Object.assign(document.createElement('link'), { + href: url, + rel: 'stylesheet', + }) + $link.onload = () => { + const $el = document.getElementById(id); + // NOTE: Maybe create and append to body if no root? + if ($el) { + manageElement($el) + resolve(id) + } else { + console.error(`[CSSX] Unable to find root for ${id}`) + reject(`[CSSX] Unable to find root for ${id}`) + } + } + document.body.appendChild($link); + }), + getVariable: async varName => getPropertyValue($element, varName), + updateVariable: async (targetId, varName, value) => { + const $el = document.getElementById(targetId) + if ($el) { + $el.style.setProperty(varName, JSON.stringify(value)) + } + }, }) const handleEvents = async ($element: Element) => { @@ -50,7 +75,7 @@ const handleEvents = async ($element: Element) => { console.log(`Triggered event: ${event}`) const exprs = parse(handlerExpr) for (const expr of exprs) { - await evalExpr(expr, evalDeps($element)) + await evalExpr(expr, getEvalActions($element)) } } } @@ -73,6 +98,7 @@ const manageElement = async ($element: Element) => { $element.appendChild($childrenRoot) for (const id of childrenIds) { + // TODO: Allow adding node types other than div const $child = $childrenRoot.querySelector(`:scope > #${id}`) ?? Object.assign(document.createElement('div'), { id }) diff --git a/tests/eval.spec.ts b/tests/eval.spec.ts index 63062e3..8b7424f 100644 --- a/tests/eval.spec.ts +++ b/tests/eval.spec.ts @@ -1,10 +1,15 @@ -import { Dependencies, evalExpr } from '../src/eval' +import { EvalActions, evalExpr } from '../src/eval' import { Expr } from '../src/parser' describe('eval', () => { - const deps: Dependencies = { + const deps: EvalActions = { addClass: jest.fn(), removeClass: jest.fn(), + delay: jest.fn(), + jsEval: jest.fn(), + loadCssx: jest.fn(), + getVariable: jest.fn(), + updateVariable: jest.fn(), } it('should add classes', async () => { @@ -17,7 +22,7 @@ describe('eval', () => { expect(deps.addClass).toHaveBeenCalledWith('element-id', 'class-name') }) - it('should add classes', async () => { + it('should remove classes', async () => { await evalExpr(Expr.Call({ name: 'remove-class', args: [ Expr.Identifier('element-id'), Expr.LiteralString('class-name') ], @@ -26,4 +31,24 @@ describe('eval', () => { expect(deps.removeClass).toHaveBeenCalledTimes(1) expect(deps.removeClass).toHaveBeenCalledWith('element-id', 'class-name') }) + + it('should add a delay', async () => { + await evalExpr(Expr.Call({ + name: 'delay', + args: [ Expr.LiteralString('200') ], + }), deps) + + expect(deps.delay).toHaveBeenCalledTimes(1) + expect(deps.delay).toHaveBeenCalledWith(200) + }) + + it('should get variable', async () => { + await evalExpr(Expr.Call({ + name: 'var', + args: [ Expr.LiteralString('--my-var'), Expr.LiteralString('def value') ], + }), deps) + + expect(deps.getVariable).toHaveBeenCalledTimes(1) + expect(deps.getVariable).toHaveBeenCalledWith('--my-var') + }) }) -- cgit v1.3.1