Skip to content

Commit

Permalink
feat(model): add type modifiers (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
lowlighter authored Nov 10, 2024
1 parent 3eef60a commit 9e48ce0
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 6 deletions.
31 changes: 25 additions & 6 deletions @mizu/model/mod.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Imports
import { type Cache, type callback, type Directive, type Modifiers, type Nullable, Phase } from "@mizu/internal/engine"
import { type Arg, type Arrayable, type Cache, type callback, type Directive, type Modifiers, type Nullable, Phase } from "@mizu/internal/engine"
import { equal } from "@std/assert"
import { _event } from "@mizu/event"
export type * from "@mizu/internal/engine"
Expand All @@ -13,6 +13,10 @@ export const typings = {
throttle: { type: Date, default: 250 },
debounce: { type: Date, default: 250 },
keys: { type: String },
nullish: { type: Boolean },
boolean: { type: Boolean },
number: { type: Boolean },
string: { type: Boolean },
},
} as const

Expand Down Expand Up @@ -98,12 +102,12 @@ export const _model_value = {
// Radio: clean value if last value unchecked
case "radio":
if ((equal(model, value)) && (!input.checked)) {
value = undefined
value = undefined as unknown as typeof value
}
break
// Number: convert value to number
case "number":
value = Number(value)
value = parse(Number(value) as unknown as Arg<typeof parse>, parsed.modifiers)
}
await renderer.evaluate(input, `${attribute.value}=${renderer.internal("value")}`, { ...options, state: { ...options.state, [renderer.internal("value")]: value } })
}
Expand Down Expand Up @@ -133,7 +137,7 @@ export default [_model_value]

/** Read input value. */
function read(input: HTMLElement) {
let value = null as unknown
let value = null as Nullable<Arrayable<string>>
switch (input.tagName) {
case "SELECT": {
const select = input as HTMLSelectElement
Expand All @@ -151,6 +155,21 @@ function read(input: HTMLElement) {
}

/** Parse input value. */
function parse(value: unknown, _modifiers: Modifiers<typeof _model_value>) {
return value
function parse(value: ReturnType<typeof read>, modifiers: Modifiers<typeof _model_value>) {
const parsed = [value].flat().map((value) => {
if ((modifiers.nullish) && (!value)) {
return null
}
if (modifiers.number) {
return Number(value)
}
if (modifiers.boolean) {
return !(value.length && /^(?:[Ff]alse|FALSE|[Nn]o|NO|[Oo]ff|OFF)$/.test(value))
}
if (modifiers.string) {
return `${value}`
}
return value
})
return Array.isArray(value) ? parsed : parsed[0]
}
176 changes: 176 additions & 0 deletions @mizu/model/mod_test.html
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,182 @@
</expect>
</test>

<test name="[::value] with `.nullish` modifier casts to `null` if empty">
<script>
context.value = []
</script>
<render>
<input type="checkbox" name="raw" ::="value" value="" />
<input type="checkbox" name="casted" ::.nullish="value" value="" />
</render>
<script>
// Raw value
raw = document.querySelector("input[name=raw]")
raw.checked = true
raw.dispatchEvent(new Event("input"))
await retry(() => expect(context.value).toEqual([""]))
// Casted value
casted = document.querySelector("input[name=casted]")
casted.checked = true
casted.dispatchEvent(new Event("input"))
await retry(() => expect(context.value).toEqual(["", null]))
// Reverse mapping
context.value = [""]
</script>
<render></render>
<script>
expect(document.querySelector("input[name=raw]").checked).toBe(true)
expect(document.querySelector("input[name=casted]").checked).toBe(false)
context.value = [null]
</script>
<render></render>
<script>
expect(document.querySelector("input[name=raw]").checked).toBe(false)
expect(document.querySelector("input[name=casted]").checked).toBe(true)
</script>
</test>

<test name="[::value] with `.number` modifier casts to `Number()`">
<script>
context.value = []
</script>
<render>
<input type="checkbox" name="raw" ::="value" value="0" />
<input type="checkbox" name="casted" ::.number="value" value="0" />
</render>
<script>
// Raw value
raw = document.querySelector("input[name=raw]")
raw.checked = true
raw.dispatchEvent(new Event("input"))
await retry(() => expect(context.value).toEqual(["0"]))
// Casted value
casted = document.querySelector("input[name=casted]")
casted.checked = true
casted.dispatchEvent(new Event("input"))
await retry(() => expect(context.value).toEqual(["0", 0]))
// Reverse mapping
context.value = ["0"]
</script>
<render></render>
<script>
expect(document.querySelector("input[name=raw]").checked).toBe(true)
expect(document.querySelector("input[name=casted]").checked).toBe(false)
context.value = [0]
</script>
<render></render>
<script>
expect(document.querySelector("input[name=raw]").checked).toBe(false)
expect(document.querySelector("input[name=casted]").checked).toBe(true)
</script>
</test>

<test name="[::value] with `.boolean` modifier casts to YAML-like `Boolean()`">
<script>
context.value = []
</script>
<render>
<input type="checkbox" name="raw" ::="value" value="yes" />
<input type="checkbox" name="casted" ::.boolean="value" value="yes" />
</render>
<script>
// Raw value
raw = document.querySelector("input[name=raw]")
raw.checked = true
raw.dispatchEvent(new Event("input"))
await retry(() => expect(context.value).toEqual(["yes"]))
// Casted value
casted = document.querySelector("input[name=casted]")
casted.checked = true
casted.dispatchEvent(new Event("input"))
await retry(() => expect(context.value).toEqual(["yes", true]))
// Reverse mapping
context.value = ["yes"]
</script>
<render></render>
<script>
expect(document.querySelector("input[name=raw]").checked).toBe(true)
expect(document.querySelector("input[name=casted]").checked).toBe(false)
context.value = [true]
</script>
<render></render>
<script>
expect(document.querySelector("input[name=raw]").checked).toBe(false)
expect(document.querySelector("input[name=casted]").checked).toBe(true)
</script>
<script>
context.value = []
</script>
<render>
<input type="checkbox" name="raw" ::="value" value="no" />
<input type="checkbox" name="casted" ::.boolean="value" value="no" />
</render>
<script>
// Raw value
raw = document.querySelector("input[name=raw]")
raw.checked = true
raw.dispatchEvent(new Event("input"))
await retry(() => expect(context.value).toEqual(["no"]))
// Casted value
casted = document.querySelector("input[name=casted]")
casted.checked = true
casted.dispatchEvent(new Event("input"))
await retry(() => expect(context.value).toEqual(["no", false]))
// Reverse mapping
context.value = ["no"]
</script>
<render></render>
<script>
expect(document.querySelector("input[name=raw]").checked).toBe(true)
expect(document.querySelector("input[name=casted]").checked).toBe(false)
context.value = [false]
</script>
<render></render>
<script>
expect(document.querySelector("input[name=raw]").checked).toBe(false)
expect(document.querySelector("input[name=casted]").checked).toBe(true)
</script>
</test>

<test name="[::value] with `.string` modifier casts to `String()`">
<script>
context.value = { raw: null, casted: null }
</script>
<render>
<input type="number" name="raw" ::="value.raw" />
<input type="number" name="casted" ::.string="value.casted" />
</render>
<script>
// Raw value
raw = document.querySelector("input[name=raw]")
raw.value = "0"
raw.dispatchEvent(new Event("input"))
await retry(() => expect(context.value.raw).toBe(0))
// Casted value
casted = document.querySelector("input[name=casted]")
casted.value = "0"
casted.dispatchEvent(new Event("input"))
await retry(() => expect(context.value.casted).toBe("0"))
</script>
</test>

<test name="[::value] with type modifier supports `&lt;select multiple&gt;`">
<script>
context.value = []
</script>
<render>
<select ::.number="value" multiple>
<option value="1" selected></option>
<option value="2" selected></option>
</select>
</render>
<script>
document.querySelectorAll("option").forEach((option) => option.selected = true)
document.querySelector("select").dispatchEvent(new Event("input"))
await retry(() => expect(context.value).toEqual([1, 2]))
</script>
</test>

<test name="[::value] skips commented elements">
<script>
context.value = true
Expand Down

0 comments on commit 9e48ce0

Please sign in to comment.