From 695365fa359fb98f14d06c205159739077f67fed Mon Sep 17 00:00:00 2001 From: Akshay Nair Date: Fri, 11 Aug 2023 20:42:02 +0530 Subject: feat: complets form submit example --- TODO.norg | 4 +-- examples/api/content.css | 26 ---------------- examples/api/signup.css | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ examples/api/style.css | 32 +++++++++++--------- src/eval.ts | 27 +++++++++++++++++ src/index.ts | 41 +++++++++++++++++++------- tests/eval.spec.ts | 1 + 7 files changed, 155 insertions(+), 53 deletions(-) delete mode 100644 examples/api/content.css create mode 100644 examples/api/signup.css diff --git a/TODO.norg b/TODO.norg index f85a80f..31a0413 100644 --- a/TODO.norg +++ b/TODO.norg @@ -4,8 +4,8 @@ - (x) `get-variable` - (x) `update-variable` - (x) Use css units for `delay` function - - ( ) Specify node type - `button(id)` or `button#id` - - ( ) `--cssx-attr-*` for attributes + - (x) Specify node type - `button(id)` or `button#id` + - (x) attributes - ( ) `--cssx-text` (and maybe `--cssx-html`?) - ( ) Improve error messages - ( ) Evaluate `calc`? diff --git a/examples/api/content.css b/examples/api/content.css deleted file mode 100644 index 5463ba2..0000000 --- a/examples/api/content.css +++ /dev/null @@ -1,26 +0,0 @@ - -#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/signup.css b/examples/api/signup.css new file mode 100644 index 0000000..c5970c7 --- /dev/null +++ b/examples/api/signup.css @@ -0,0 +1,77 @@ + +#signup-page-content { + border: 1px solid #888; + padding: 1rem; + max-width: 700px; + margin: 1rem auto; + + --cssx-children: form#form; + --count: '0'; +} + +#signup-page-content::before { + content: "Sign-Up"; + display: block; + font-size: 2rem; + border-bottom: 1px solid gray; +} + +#form { + display: block; + + --cssx-on-submit: + prevent-default() + add-class(form, 'submitting') + request('https://httpbin.org/post', POST) + remove-class(form, 'submitting') + add-class(form, 'submitted') + ; + + --cssx-children: input#input-email input#input-password actions #message; +} +#form.submitted #message::after { + display: block; + content: "Form submitted successfully"; +} +#form.submitting #submit-btn { + pointer-events: none; + opacity: .5; +} + +#form input { + display: block; + width: 100%; + padding: 0.4rem 0.8rem; + margin-top: 1rem; +} + +#input-email { + --cssx-on-mount: + set-attr('type', 'email') + set-attr('name', 'email') + set-attr('required', 'true') + set-attr('placeholder', 'Email. Eg:- mail@postbox.com') + ; +} + +#input-password { + --cssx-on-mount: + set-attr('type', 'password') + set-attr('name', 'password') + set-attr('required', 'true') + set-attr('placeholder', 'Password. Eg:- password, password1, password2, password123') + ; +} + +#actions { + text-align: right; + padding-top: 1rem; + --cssx-children: button#submit-btn; +} + +#submit-btn { + padding: 0.4rem 0.7rem; + --cssx-on-mount: set-attr('type', 'submit'); +} +#submit-btn::after { content: "Sign-Up"; } + diff --git a/examples/api/style.css b/examples/api/style.css index cdf4559..b79f265 100644 --- a/examples/api/style.css +++ b/examples/api/style.css @@ -1,34 +1,38 @@ body { - --cssx-children: button#load-btn output-container; + --cssx-children: button#signup-btn signup-page; +} +body * { + box-sizing: border-box; } -#load-btn { +#signup-btn { display: inline-block; background: #5180e9; color: #000; padding: 0.5rem 1rem; cursor: pointer; + --cssx-on-mount: set-attr('type', 'button'); + --cssx-on-click: - add-class(output-container, 'loading') - add-class(load-btn, 'loading') - load-cssx(output-container-content, './content.css') - remove-class(output-container, 'loading') - remove-class(load-btn, 'loading') - delay(0.7s) - js-eval('alert("Loaded page")') + add-class(signup-page, 'loading') + add-class(signup-btn, 'loading') + delay(0.5s) + load-cssx(signup-page-content, './signup.css') + remove-class(signup-page, 'loading') + remove-class(signup-btn, 'loading') ; } -#load-btn::after { content: "Click me for magic"; } -#load-btn.loading { +#signup-btn::after { content: "Register now to start your free trail for $99"; } +#signup-btn.loading { pointer-events: none; opacity: 0.4; } -#output-container { - --cssx-children: output-container-content; +#signup-page { + --cssx-children: signup-page-content; } -#output-container.loading::after { +#signup-page.loading::after { content: "Loading..."; } diff --git a/src/eval.ts b/src/eval.ts index 466cafd..191a76b 100644 --- a/src/eval.ts +++ b/src/eval.ts @@ -9,6 +9,10 @@ export type EvalActions = { loadCssx(id: string, url: string): Promise getVariable(name: string): Promise updateVariable(id: string, varName: string, value: string): Promise + setAttribute(name: string, value: string): Promise + withEvent(fn: (e: any) => void): Promise + getFormData(): Promise + sendRequest(_: { method: string, url: string, data: FormData | undefined }): Promise // calculate ?? } @@ -47,6 +51,7 @@ const getFunctions = (name: string, args: Expr[], actions: EvalActions) => await actions.removeClass(id, classes) } }, + delay: async () => { const num = await evalExpr(args[0], actions) console.log(num) @@ -56,6 +61,7 @@ const getFunctions = (name: string, args: Expr[], actions: EvalActions) => 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) @@ -63,6 +69,7 @@ const getFunctions = (name: string, args: Expr[], actions: EvalActions) => await actions.loadCssx(id, url) } }, + var: async () => { const varName = await evalExpr(args[0], actions) const defaultValue = await evalExpr(args[1], actions) @@ -76,5 +83,25 @@ const getFunctions = (name: string, args: Expr[], actions: EvalActions) => actions.updateVariable(id, varName, value) } }, + + 'set-attr': async () => { + const name = await evalExpr(args[0], actions) + const value = await evalExpr(args[1], actions) + if (name && value) { + actions.setAttribute(name, value) + } + }, + 'prevent-default': async () => actions.withEvent(e => e.preventDefault()), + + 'request': async () => { + const url = await evalExpr(args[0], actions) + const method = args[1] ? (await evalExpr(args[1], actions) ?? 'post') : 'post' + + if (url) { + const data = await actions.getFormData() + await actions.sendRequest({ method, url, data }) + } + }, + _: () => Promise.reject(new Error('not supposed to be here')), }) diff --git a/src/index.ts b/src/index.ts index 7e73cf5..88347df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ const UNSET_PROPERTY_VALUE = '' const EVENT_HANDLERS = { click: '--cssx-on-click', load: '--cssx-on-load', + mount: '--cssx-on-mount', + submit: '--cssx-on-submit', } const injectStyles = () => { @@ -33,7 +35,7 @@ const getChildrenIds = ($element: Element) => { return value.split(/(\s*,\s*)|\s+/g).filter(Boolean) } -const getEvalActions = ($element: Element): EvalActions => ({ +const getEvalActions = ($element: Element, event: any): EvalActions => ({ addClass: async (id, cls) => document.getElementById(id)?.classList.add(cls), removeClass: async (id, cls) => document.getElementById(id)?.classList.remove(cls), @@ -65,29 +67,44 @@ const getEvalActions = ($element: Element): EvalActions => ({ $el.style.setProperty(varName, JSON.stringify(value)) } }, + setAttribute: async (name, value) => { + $element.setAttribute(name, value) + }, + withEvent: async (fn) => fn(event), + getFormData: async () => $element.nodeName === 'FORM' ? new FormData($element as HTMLFormElement) : undefined, + sendRequest: async ({ url, method, data }) => { + await fetch(url, { method, body: data }) + // TODO: Handle response? + }, }) -const handleEvents = async ($element: Element) => { - for (const [event, property] of Object.entries(EVENT_HANDLERS)) { +const handleEvents = async ($element: Element, isNewElement: boolean = false) => { + for (const [eventType, property] of Object.entries(EVENT_HANDLERS)) { const handlerExpr = getPropertyValue($element, property) if (handlerExpr) { - ;($element as any)[`on${event}`] = async () => { - console.log(`Triggered event: ${event}`) + const eventHandler = async (event: any) => { + console.log(`Triggered event: ${eventType}`) const exprs = parse(handlerExpr) for (const expr of exprs) { - await evalExpr(expr, getEvalActions($element)) + await evalExpr(expr, getEvalActions($element, event)) } } + + if (eventType === 'mount') { + if (isNewElement) setTimeout(eventHandler) + } else { + ;($element as any)[`on${eventType}`] = eventHandler + } } } } let nodeCount = 0 -const manageElement = async ($element: Element) => { +const manageElement = async ($element: Element, isNewElement: boolean = false) => { if (nodeCount++ > 100) return // NOTE: Temporary. To prevent infinite rec - await handleEvents($element) + await handleEvents($element, isNewElement) const childrenIds = getChildrenIds($element) if (childrenIds.length > 0) { @@ -98,12 +115,14 @@ const manageElement = async ($element: Element) => { $element.appendChild($childrenRoot) for (const childId of childrenIds) { - const [tag, id] = childId.split('#') + let isNewElement = false; + const selector = childId.split('#') + const [tag, id] = selector.length >= 2 ? selector : ['div', ...selector] const $child = $childrenRoot.querySelector(`:scope > #${id}`) ?? - Object.assign(document.createElement(tag || 'div'), { id }) + (isNewElement = true, Object.assign(document.createElement(tag || 'div'), { id })) $childrenRoot.appendChild($child) - await manageElement($child) + await manageElement($child, isNewElement) } } } diff --git a/tests/eval.spec.ts b/tests/eval.spec.ts index 8b7424f..49ef4c4 100644 --- a/tests/eval.spec.ts +++ b/tests/eval.spec.ts @@ -10,6 +10,7 @@ describe('eval', () => { loadCssx: jest.fn(), getVariable: jest.fn(), updateVariable: jest.fn(), + setAttribute: jest.fn(), } it('should add classes', async () => { -- cgit v1.3.1