From f5e0d210e3c42fd6145344e11e542d37de0460fe Mon Sep 17 00:00:00 2001 From: Andrew Buchan Date: Tue, 7 Apr 2026 15:36:12 +0100 Subject: [PATCH] Added bind-as directive --- .../babel-plugin-wallace/src/constants.ts | 9 +++ .../babel-plugin-wallace/src/directives.ts | 18 +++++- packages/babel-plugin-wallace/src/errors.ts | 5 +- packages/wallace/lib/types.d.ts | 34 +++++++++++ .../07.directives.13.bind-as.spec.jsx | 61 +++++++++++++++++++ 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 packages/wallace/tests/functionality/07.directives.13.bind-as.spec.jsx diff --git a/packages/babel-plugin-wallace/src/constants.ts b/packages/babel-plugin-wallace/src/constants.ts index 48580eb..1a64dea 100644 --- a/packages/babel-plugin-wallace/src/constants.ts +++ b/packages/babel-plugin-wallace/src/constants.ts @@ -77,6 +77,15 @@ export enum SPECIAL_SYMBOLS { patch = "patch" } +// We don't support date-related types like time, week or datetime-local as they don't +// use valueAsDate as you'd expect. +export const INPUT_TYPE_VALUES = { + checkbox: "checked", + date: "valueAsDate", + number: "valueAsNumber", + range: "valueAsNumber" +}; + export const DOM_EVENTS = [ "Abort", "AnimationCancel", diff --git a/packages/babel-plugin-wallace/src/directives.ts b/packages/babel-plugin-wallace/src/directives.ts index 8a0980d..59a6057 100644 --- a/packages/babel-plugin-wallace/src/directives.ts +++ b/packages/babel-plugin-wallace/src/directives.ts @@ -21,7 +21,8 @@ import { WATCH_CALLBACK_ARGS, SPECIAL_SYMBOLS, IMPORTABLES, - DOM_EVENTS_LOWERCASE + DOM_EVENTS_LOWERCASE, + INPUT_TYPE_VALUES } from "./constants"; class ApplyDirective extends Directive { @@ -53,6 +54,20 @@ class BindDirective extends Directive { } } +class BindAsDirective extends Directive { + static attributeName = "bind-as"; + static valueMode: ValueMode = ValueMode.ExpressionRequired; + static qualifierMode: QualifierMode = QualifierMode.Required; + apply(node: TagNode, value: NodeValue, qualifier: Qualifier, _base: string) { + if (INPUT_TYPE_VALUES.hasOwnProperty(qualifier)) { + node.setBindInstruction(value.expression, INPUT_TYPE_VALUES[qualifier]); + node.addFixedAttribute("type", qualifier); + } else { + error(node.path, ERROR_MESSAGES.INVALID_BIND_AS_FIELD(qualifier)); + } + } +} + class ClassDirective extends Directive { static attributeName = "class"; static valueMode: ValueMode = ValueMode.EitherRequired; @@ -297,6 +312,7 @@ export const builtinDirectives = [ ApplyDirective, AssignDirective, BindDirective, + BindAsDirective, ClassDirective, CssDirective, CtrlDirective, diff --git a/packages/babel-plugin-wallace/src/errors.ts b/packages/babel-plugin-wallace/src/errors.ts index 1241654..4a926cd 100644 --- a/packages/babel-plugin-wallace/src/errors.ts +++ b/packages/babel-plugin-wallace/src/errors.ts @@ -1,5 +1,5 @@ import type { NodePath } from "@babel/core"; -import { XARGS } from "./constants"; +import { INPUT_TYPE_VALUES, XARGS } from "./constants"; const ALLOWED_XARGS: string[] = Object.values(XARGS).map(n => `"${n}"`); @@ -56,6 +56,9 @@ export const ERROR_MESSAGES = { // A nested stub // A repeated stub `, + INVALID_BIND_AS_FIELD: (field: string) => + `\`${field}\` is not a valid value. Must be one of : ${Object.keys(INPUT_TYPE_VALUES).join(", ")}. + As other inputs may not bind to the value you think.`, INVALID_EVENT_NAME: (event: string) => `\`${event}\` is not a valid event. Must be lowercase without \`on\` prefix. E.g. \`event:keyup\`.`, NESTED_COMPONENT_MUST_BE_CAPTIALIZED: "Nested component must be capitalized.", diff --git a/packages/wallace/lib/types.d.ts b/packages/wallace/lib/types.d.ts index 7dad85e..f5036ee 100644 --- a/packages/wallace/lib/types.d.ts +++ b/packages/wallace/lib/types.d.ts @@ -895,6 +895,7 @@ interface DirectiveAttributes extends AllDomEvents { * ``` * * ``` + * * By default it watches the `change` event, but you can specify a different one using * the `event` directive: * @@ -910,6 +911,39 @@ interface DirectiveAttributes extends AllDomEvents { */ bind?: MustBeExpression; + /** + * ## Wallace directive: bind-as + * + * Set input type and binding to the property you likely want for that input type: + * + * ``` + * + * + * + * + * ``` + * + * Is the equivalent of this: + * + * ``` + * + * + * + * + * ``` + * + * Other types like `month`, `time` and `datetime-local` are not supported as they + * don't use the properties you'd expect. + * + * Like `bind` it watches the `change` event, but you can specify a different one with + * the `event` directive: + * + * ``` + * + * ``` + */ + "bind-as"?: MustBeExpression; + /** * ## Wallace directive: class * diff --git a/packages/wallace/tests/functionality/07.directives.13.bind-as.spec.jsx b/packages/wallace/tests/functionality/07.directives.13.bind-as.spec.jsx new file mode 100644 index 0000000..eb46b87 --- /dev/null +++ b/packages/wallace/tests/functionality/07.directives.13.bind-as.spec.jsx @@ -0,0 +1,61 @@ +import { testMount } from "../utils"; + +describe("specification", () => { + test("with qualifier and expression", () => { + const code = `const Foo = () =>
`; + expect(code).toCompileWithoutError(); + }); + + test("must supply an expression", () => { + const code = `const Foo = () =>
`; + expect(code).toCompileWithError( + "The `bind-as` directive requires a value of type `expression`." + ); + }); + + test("must supply a qualifier", () => { + const code = `const Foo = () =>
`; + expect(code).toCompileWithError("The `bind-as` directive must have a qualifier."); + }); + + test("cannot supply unsupported qualifier", () => { + const code = `const Foo = () =>
`; + expect(code).toCompileWithError( + "`time` is not a valid value. Must be one of : checkbox, date, number, range." + ); + }); +}); + +describe("behaviour", () => { + test("checkbox", () => { + const data = true; + const MyComponent = () => ; + const component = testMount(MyComponent); + expect(component).toRender(``); + expect(component.el.checked).toStrictEqual(true); + }); + + test("date", () => { + const data = new Date(); + const MyComponent = () => ; + const component = testMount(MyComponent); + expect(component).toRender(``); + expect(component.el.valueAsDate.toDateString()).toStrictEqual(data.toDateString()); + }); + + test("number", () => { + const data = 42; + const MyComponent = () => ; + const component = testMount(MyComponent); + expect(component).toRender(``); + expect(component.el.valueAsNumber).toStrictEqual(data); + }); + + test("range", () => { + const data = 42; + const MyComponent = () => ; + const component = testMount(MyComponent); + expect(component).toRender(``); + expect(component.el.valueAsNumber).toStrictEqual(data); + }); +});