Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/babel-plugin-wallace/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 17 additions & 1 deletion packages/babel-plugin-wallace/src/directives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -297,6 +312,7 @@ export const builtinDirectives = [
ApplyDirective,
AssignDirective,
BindDirective,
BindAsDirective,
ClassDirective,
CssDirective,
CtrlDirective,
Expand Down
5 changes: 4 additions & 1 deletion packages/babel-plugin-wallace/src/errors.ts
Original file line number Diff line number Diff line change
@@ -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}"`);

Expand Down Expand Up @@ -56,6 +56,9 @@ export const ERROR_MESSAGES = {
<stub.foo ...> // A nested stub
<stub.foo.repeat ...> // 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.",
Expand Down
34 changes: 34 additions & 0 deletions packages/wallace/lib/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,7 @@ interface DirectiveAttributes extends AllDomEvents {
* ```
* <input type="text" value={name} onChange={name = event.target.value} />
* ```
*
* By default it watches the `change` event, but you can specify a different one using
* the `event` directive:
*
Expand All @@ -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:
*
* ```
* <input bind-as:checkbox={foo} />
* <input bind-as:date={foo} />
* <input bind-as:number={foo} />
* <input bind-as:range={foo} />
* ```
*
* Is the equivalent of this:
*
* ```
* <input type="checkbox" bind:checked={foo} />
* <input type="date" bind:valueAsDate={foo} />
* <input type="number" bind:valueAsNumber={foo} />
* <input type="range" bind:valueAsNumber={foo} />
* ```
*
* 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:
*
* ```
* <input bind-as:range={foo} event:input />
* ```
*/
"bind-as"?: MustBeExpression;

/**
* ## Wallace directive: class
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { testMount } from "../utils";

describe("specification", () => {
test("with qualifier and expression", () => {
const code = `const Foo = () => <div bind-as:range={foo} ></div>`;
expect(code).toCompileWithoutError();
});

test("must supply an expression", () => {
const code = `const Foo = () => <div bind-as:range="foo"></div>`;
expect(code).toCompileWithError(
"The `bind-as` directive requires a value of type `expression`."
);
});

test("must supply a qualifier", () => {
const code = `const Foo = () => <div bind-as={foo}></div>`;
expect(code).toCompileWithError("The `bind-as` directive must have a qualifier.");
});

test("cannot supply unsupported qualifier", () => {
const code = `const Foo = () => <div bind-as:time={foo}></div>`;
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 = () => <input bind-as:checkbox={data} />;
const component = testMount(MyComponent);
expect(component).toRender(`<input type="checkbox">`);
expect(component.el.checked).toStrictEqual(true);
});

test("date", () => {
const data = new Date();
const MyComponent = () => <input bind-as:date={data} />;
const component = testMount(MyComponent);
expect(component).toRender(`<input type="date">`);
expect(component.el.valueAsDate.toDateString()).toStrictEqual(data.toDateString());
});

test("number", () => {
const data = 42;
const MyComponent = () => <input bind-as:number={data} />;
const component = testMount(MyComponent);
expect(component).toRender(`<input type="number">`);
expect(component.el.valueAsNumber).toStrictEqual(data);
});

test("range", () => {
const data = 42;
const MyComponent = () => <input bind-as:range={data} />;
const component = testMount(MyComponent);
expect(component).toRender(`<input type="range">`);
expect(component.el.valueAsNumber).toStrictEqual(data);
});
});
Loading