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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "regor",
"version": "1.5.1",
"version": "1.5.2",
"description": "A modern UI framework for web and desktop applications, inspired by Vue's concepts and powered by simplicity and flexibility.",
"author": "Ahmed Yasin Koculu",
"license": "MIT",
Expand Down
32 changes: 28 additions & 4 deletions src/app/propValidators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ type ValidatorTuple = readonly [
]
type InferValidatorValue<TValidator> =
TValidator extends PropValidator<infer TValue> ? TValue : never
type ShapeSchemaFor<TShape extends object> = {
[TKey in keyof TShape]?: PropValidator<TShape[TKey]>
}

/**
* Validation schema shape suggested by `ComponentHead<T>.props`.
Expand Down Expand Up @@ -392,16 +395,37 @@ export const arrayOf = <TValue>(
/**
* Validates that a prop is an object and applies validators to selected nested
* keys. Nested failures include dotted prop paths such as `meta.slug`.
*
* `shape(...)` supports both inferred literal schemas and explicit generic
* target types for editor completion:
*
* ```ts
* pval.shape({
* tabId: pval.isNumber,
* })
*
* pval.shape<TabPane>({
* tabId: pval.isNumber,
* })
* ```
*/
export const shape = <TSchema extends ValidationSchemaLike>(
export function shape<TShape extends object>(
schema: ShapeSchemaFor<TShape>,
): PropValidator<TShape>
export function shape<TSchema extends ValidationSchemaLike>(
schema: TSchema,
): PropValidator<InferPropValidationSchema<TSchema>> => {
): PropValidator<InferPropValidationSchema<TSchema>>
export function shape(
schema: Record<string, PropValidator<any> | undefined>,
): PropValidator<Record<string, unknown>> {
return (value, name, head) => {
if (!isObject(value)) fail(name, `expected object, ${describe(value)}`)
const record = value as Record<string, unknown>
for (const key in schema) {
const validator: PropValidator<unknown> = schema[key]
validator(record[key], `${name}.${key}`, head)
const validator: PropValidator<unknown> | undefined = schema[key]
if (!validator) continue
const validateField: PropValidator<unknown> = validator
validateField(record[key], `${name}.${key}`, head)
}
}
}
Expand Down
59 changes: 59 additions & 0 deletions tests/app/component-head.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,65 @@ test('component head validator utilities support nested or validation inside sha
)
})

test('component head validator utilities support explicit generic shape schemas', () => {
type TabPaneType = {
tabId: number
title: string
closable?: boolean
}

interface TabPaneInterface {
tabId: number
title: string
disabled?: boolean
}

class TabPaneClass {
tabId = 0
title = ''
visible?: boolean
}

const host = document.createElement('div')
const start = document.createComment('s')
const end = document.createComment('e')
const head = new ComponentHead(
{
typePane: {
tabId: 1,
title: 'Type pane',
},
interfacePane: {
tabId: 2,
title: 'Interface pane',
},
classPane: {
tabId: 3,
title: 'Class pane',
},
},
host,
[],
start,
end,
'throw',
)

expect(() =>
head.validateProps({
typePane: pval.shape<TabPaneType>({
tabId: pval.isNumber,
}),
interfacePane: pval.shape<TabPaneInterface>({
tabId: pval.isNumber,
}),
classPane: pval.shape<TabPaneClass>({
tabId: pval.isNumber,
}),
}),
).not.toThrow()
})

test('component head validateProps accepts custom user validators', () => {
const host = document.createElement('div')
const start = document.createComment('s')
Expand Down
Loading