diff --git a/src/formio/components/index.ts b/src/formio/components/index.ts index 9acb96b..605f02a 100644 --- a/src/formio/components/index.ts +++ b/src/formio/components/index.ts @@ -3,6 +3,9 @@ export * from './textfield'; export * from './email'; export * from './date'; export * from './datetime'; +export * from './time'; +export * from './phonenumber'; +export * from './postcode'; export * from './number'; // Layout components diff --git a/src/formio/components/phonenumber.ts b/src/formio/components/phonenumber.ts new file mode 100644 index 0000000..ad691a3 --- /dev/null +++ b/src/formio/components/phonenumber.ts @@ -0,0 +1,23 @@ +import {InputComponentSchema, MultipleCapable} from '..'; + +type Validator = 'required' | 'pattern'; +type TranslatableKeys = 'label' | 'description' | 'tooltip'; + +export type PhoneNumberInputSchema = InputComponentSchema; + +/** + * @group Form.io components + * @category Base types + */ +export interface BasePhoneNumberComponentSchema extends Omit { + type: 'phoneNumber'; + inputMask: null; + // additional properties + autocomplete?: string; +} + +/** + * @group Form.io components + * @category Concrete types + */ +export type PhoneNumberComponentSchema = MultipleCapable; diff --git a/src/formio/components/postcode.ts b/src/formio/components/postcode.ts new file mode 100644 index 0000000..f8fc848 --- /dev/null +++ b/src/formio/components/postcode.ts @@ -0,0 +1,43 @@ +import {InputComponentSchema, MultipleCapable, PrefillConfig} from '..'; + +type Validator = 'required' | 'pattern' | 'customMessage'; +type TranslatableKeys = 'label' | 'description' | 'tooltip'; + +export type PostCodeInputSchema = InputComponentSchema; + +/** + * The textfield component properties that configure it for Dutch postal codes. + * + * @group Form.io components + * @category Base types + */ +export interface PostcodeProperties { + inputMask: '9999 AA'; + validate: { + // Dutch postcode has 4 numbers and 2 letters (case insensitive). Letter combinations SS, SD and SA + // are not used due to the Nazi-association. + // See https://stackoverflow.com/a/17898538/7146757 and https://nl.wikipedia.org/wiki/Postcodes_in_Nederland + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$'; + }; + validateOn: 'blur'; +} + +/** + * @group Form.io components + * @category Base types + * @deprecated Use textfield instead, with the additional hardcoded properties. + */ +export type BasePostcodeComponentSchema = Omit & + PrefillConfig & + PostcodeProperties & { + type: 'postcode'; + // additional properties + autocomplete?: string; + }; + +/** + * @group Form.io components + * @category Concrete types + * @deprecated Use textfield instead, with the additional hardcoded properties. + */ +export type PostcodeComponentSchema = MultipleCapable; diff --git a/src/formio/components/time.ts b/src/formio/components/time.ts new file mode 100644 index 0000000..4f62120 --- /dev/null +++ b/src/formio/components/time.ts @@ -0,0 +1,31 @@ +import {InputComponentSchema, MultipleCapable} from '..'; + +type Validator = 'required' | 'minTime' | 'maxTime'; +type TranslatableKeys = 'label' | 'description' | 'tooltip'; + +export type TimeInputSchema = InputComponentSchema; + +/** + * @group Form.io components + * @category Base types + */ +export interface BaseTimeComponentSchema extends Omit { + type: 'time'; + // hardcoded in builder + inputType: 'text'; + format: 'HH:mm'; + validateOn: 'blur'; +} + +/** + * A time component schema. + * + * Note that the value/`defaultValue` type is just a plain string - a serialized + * ISO-8601 time. + * + * The smallest supported resolution is minutes, seconds are truncated to be 0 seconds. + * + * @group Form.io components + * @category Concrete types + */ +export type TimeComponentSchema = MultipleCapable; diff --git a/src/formio/index.ts b/src/formio/index.ts index 1624dbd..bf0a86a 100644 --- a/src/formio/index.ts +++ b/src/formio/index.ts @@ -1,9 +1,13 @@ import { ContentComponentSchema, DateComponentSchema, + DateTimeComponentSchema, EmailComponentSchema, NumberComponentSchema, + PhoneNumberComponentSchema, + PostcodeComponentSchema, TextFieldComponentSchema, + TimeComponentSchema, } from './components'; /** @@ -34,6 +38,10 @@ export type AnyComponentSchema = | TextFieldComponentSchema | EmailComponentSchema | DateComponentSchema + | DateTimeComponentSchema + | TimeComponentSchema + | PhoneNumberComponentSchema + | PostcodeComponentSchema | NumberComponentSchema // layout | ContentComponentSchema; diff --git a/src/formio/validation.ts b/src/formio/validation.ts index 827594c..0bf5f90 100644 --- a/src/formio/validation.ts +++ b/src/formio/validation.ts @@ -3,7 +3,12 @@ import {ValidateOptions} from 'formiojs'; // extend formio's validate interface with our custom extension(s) declare module 'formiojs' { interface ValidateOptions { + // it's not a validator but formio uses it and we can provide translations support + // in the future + customMessage?: string; plugins?: string[]; + minTime?: string | null; + maxTime?: string | null; } } @@ -21,7 +26,12 @@ export type BaseErrorKeys = | 'invalid_email' | 'pattern' | 'minDate' - | 'maxDate'; + | 'maxDate' + | 'customMessage' + // custom, added by OF + | 'minTime' + | 'maxTime' + | 'invalid_time'; export type ComponentErrors = { [K in Keys]?: string; @@ -50,6 +60,7 @@ export type CuratedValidatorNames = keyof CuratedValidateOptions; type ValidatorToErrorMap = Required<{[K in CuratedValidatorNames]: BaseErrorKeys}>; const VALIDATOR_TO_ERROR_KEY = { + customMessage: 'customMessage', required: 'required', min: 'min', max: 'max', @@ -58,6 +69,9 @@ const VALIDATOR_TO_ERROR_KEY = { // 'email': 'invalid_email', // email component is exposed, but adds the validation implicitly minDate: 'minDate', maxDate: 'maxDate', + // custom, for time component + minTime: 'minTime' as 'minTime' | 'invalid_time', + maxTime: 'maxTime' as 'maxTime' | 'invalid_time', } as const satisfies ValidatorToErrorMap; // infer valid component error keys from the mapping of validation error code to the diff --git a/test-d/formio/components/phonenumber.test-d.ts b/test-d/formio/components/phonenumber.test-d.ts new file mode 100644 index 0000000..2952f85 --- /dev/null +++ b/test-d/formio/components/phonenumber.test-d.ts @@ -0,0 +1,164 @@ +import {expectAssignable, expectNotAssignable} from 'tsd'; + +import {PhoneNumberComponentSchema} from '../../../lib/'; + +// minimal textfield component schema +expectAssignable({ + id: 'yejak', + type: 'phoneNumber', + key: 'someInput', + label: 'Some input', + inputMask: null, +}); + +// with additional, phonenumber-component specific properties +expectAssignable({ + id: 'yejak', + type: 'phoneNumber', + key: 'someInput', + label: 'Some input', + inputMask: null, + placeholder: 'tel', +}); + +// multiple false and appropriate default value type +expectAssignable({ + id: 'yejak', + type: 'phoneNumber', + key: 'someInput', + label: 'Some input', + inputMask: null, + multiple: false, + defaultValue: '', +}); +// multiple true and appropriate default value type +expectAssignable({ + id: 'yejak', + type: 'phoneNumber', + key: 'someInput', + label: 'Some input', + inputMask: null, + multiple: true, + defaultValue: [''], +}); + +// full, correct schema +expectAssignable({ + id: 'yejak', + type: 'phoneNumber', + inputMask: null, + // basic tab in builder form + label: 'Some input', + key: 'someInput', + description: 'A description', + tooltip: 'A tooltip', + showInSummary: true, + showInEmail: false, + showInPDF: true, + multiple: false, + hidden: false, + clearOnHide: true, + isSensitiveData: true, + defaultValue: '', + autocomplete: 'tel', + // advanced tab in builder form + conditional: { + show: undefined, + when: undefined, + eq: undefined, + }, + // validation tab in builder form + validate: { + required: false, + plugins: ['phonenumber-international'], + pattern: '', + }, + translatedErrors: { + nl: { + required: 'Je moet een waarde opgeven!!!', + pattern: 'Enkel getallen toegestaan.', + }, + }, + errors: { + // translatedErrors is converted into errors by the backend + required: 'Je moet een waarde opgeven!!!', + pattern: 'Enkel getallen toegestaan.', + }, + // registration tab in builder form + registration: { + attribute: '', + }, + // translations tab in builder form + openForms: { + translations: { + nl: { + label: 'foo', + description: 'bar', + }, + }, + }, +}); + +// different component type +expectNotAssignable({ + id: 'yejak', + type: 'textfield', + key: 'someInput', + label: 'Some input', + inputMask: null, +} as const); + +// using unsupported properties +expectNotAssignable({ + id: 'yejak', + type: 'phoneNumber', + key: 'someInput', + label: 'Some input', + inputMask: null, + showCharCount: true, +} as const); + +// incorrect, invalid validator key +expectNotAssignable({ + id: 'yejak', + type: 'phoneNumber', + key: 'someInput', + label: 'Some input', + inputMask: null, + validate: { + maxLength: 100, + }, +} as const); + +// invalid, multiple true and non-array default value +expectNotAssignable({ + id: 'yejak', + type: 'phoneNumber', + key: 'someInput', + label: 'Some input', + inputMask: null, + multiple: true, + defaultValue: '', +} as const); + +// invalid, multiple false and array default value +expectNotAssignable({ + id: 'yejak', + type: 'phoneNumber', + key: 'someInput', + label: 'Some input', + inputMask: null, + multiple: false, + defaultValue: [''], +} as const); + +// invalid, multiple true and wrong default value in array element +expectNotAssignable({ + id: 'yejak', + type: 'phoneNumber', + key: 'someInput', + label: 'Some input', + inputMask: null, + multiple: true, + defaultValue: [123], +} as const); diff --git a/test-d/formio/components/postcode.test-d.ts b/test-d/formio/components/postcode.test-d.ts new file mode 100644 index 0000000..ea021dd --- /dev/null +++ b/test-d/formio/components/postcode.test-d.ts @@ -0,0 +1,203 @@ +import {expectAssignable, expectNotAssignable} from 'tsd'; + +import {PostcodeComponentSchema} from '../../../lib/'; + +// minimal postcode component schema +expectAssignable({ + id: 'yejak', + type: 'postcode', + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + validateOn: 'blur', +}); + +// with additional, phonenumber-component specific properties +expectAssignable({ + id: 'yejak', + type: 'postcode', + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + validateOn: 'blur', + autocomplete: 'postal-code', +}); + +// multiple false and appropriate default value type +expectAssignable({ + id: 'yejak', + type: 'postcode', + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + validateOn: 'blur', + multiple: false, + defaultValue: '1015 CJ', +}); + +// multiple true and appropriate default value type +expectAssignable({ + id: 'yejak', + type: 'postcode', + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + validateOn: 'blur', + multiple: true, + defaultValue: ['1015 CJ'], +}); + +// full, correct schema +expectAssignable({ + id: 'yejak', + type: 'postcode', + inputMask: '9999 AA', + validateOn: 'blur', + // basic tab in builder form + label: 'Some input', + key: 'someInput', + description: 'A description', + tooltip: 'A tooltip', + showInSummary: true, + showInEmail: false, + showInPDF: true, + multiple: false, + hidden: false, + clearOnHide: true, + isSensitiveData: true, + defaultValue: '', + disabled: false, + // advanced tab in builder form + conditional: { + show: undefined, + when: undefined, + eq: undefined, + }, + // validation tab in builder form + validate: { + required: false, + plugins: [], + // FIXED/constant, can't be edited + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + translatedErrors: { + nl: { + required: 'Je moet een waarde opgeven!!!', + }, + }, + errors: { + // translatedErrors is converted into errors by the backend + required: 'Je moet een waarde opgeven!!!', + }, + // registration tab in builder form + registration: { + attribute: '', + }, + // translations tab in builder form + openForms: { + translations: { + nl: { + label: 'foo', + description: 'bar', + }, + }, + }, +}); + +// different component type +expectNotAssignable({ + id: 'yejak', + type: 'textfield', // TODO: in the future this may become a specialized textfield alias? + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + validateOn: 'blur', +} as const); + +// using unsupported properties +expectNotAssignable({ + id: 'yejak', + type: 'postcode', + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + validateOn: 'blur', + placeholder: 'no placeholder', +} as const); + +// incorrect, invalid validator key +expectNotAssignable({ + id: 'yejak', + type: 'postcode', + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + maxLength: 7, + }, + validateOn: 'blur', +} as const); + +// invalid, multiple true and non-array default value +expectNotAssignable({ + id: 'yejak', + type: 'postcode', + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + validateOn: 'blur', + multiple: true, + defaultValue: '', +} as const); + +// invalid, multiple false and array default value +expectNotAssignable({ + id: 'yejak', + type: 'postcode', + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + validateOn: 'blur', + multiple: false, + defaultValue: [''], +} as const); + +// invalid, multiple true and wrong default value in array element +expectNotAssignable({ + id: 'yejak', + type: 'postcode', + key: 'someInput', + label: 'Some input', + inputMask: '9999 AA', + validate: { + pattern: '^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$', + }, + validateOn: 'blur', + multiple: true, + defaultValue: [123], +} as const); diff --git a/test-d/formio/components/time.test-d.ts b/test-d/formio/components/time.test-d.ts new file mode 100644 index 0000000..77cbc7a --- /dev/null +++ b/test-d/formio/components/time.test-d.ts @@ -0,0 +1,161 @@ +import {expectAssignable, expectNotAssignable} from 'tsd'; + +import {TimeComponentSchema} from '../../../lib'; + +// minimal time component schema +expectAssignable({ + id: 'ezftxdl', + type: 'time', + key: 'someTime', + label: 'Some time', + inputType: 'text', + format: 'HH:mm', + validateOn: 'blur', +}); + +// with translated error messages - the multiple messages is special here +expectAssignable({ + id: 'ezftxdl', + type: 'time', + key: 'someTime', + label: 'Some time', + inputType: 'text', + format: 'HH:mm', + validateOn: 'blur', + translatedErrors: { + nl: { + required: '', + minTime: 'Moet minimum XYZ zijn', + invalid_time: 'Ongeldige tijd opgegeven', + }, + en: { + required: '', + maxTime: 'Must be maximum XYZ', + }, + }, +}); + +// multiple false and appropriate default value type +expectAssignable({ + id: 'ezftxdl', + type: 'time', + key: 'someTime', + label: 'Some time', + inputType: 'text', + format: 'HH:mm', + validateOn: 'blur', + multiple: false, + defaultValue: '09:47', +}); + +// multiple true and appropriate default value type +expectAssignable({ + id: 'ezftxdl', + type: 'time', + key: 'someTime', + label: 'Some time', + inputType: 'text', + format: 'HH:mm', + validateOn: 'blur', + multiple: true, + defaultValue: ['12:15'], +}); + +// full, correct schema +expectAssignable({ + id: 'ezftxdl', + type: 'time', + inputType: 'text', + format: 'HH:mm', + validateOn: 'blur', + // basic tab + label: 'Some time', + key: 'someTime', + description: '', + tooltip: 'A tooltip', + showInSummary: true, + showInEmail: false, + showInPDF: true, + multiple: false, + hidden: false, + clearOnHide: true, + isSensitiveData: false, + defaultValue: '', + // Advanced tab + conditional: { + show: undefined, + when: '', + eq: '', + }, + // Validation tab + validate: { + required: false, + plugins: [], + minTime: '10:00', + maxTime: '20:00', + }, + translatedErrors: { + nl: { + required: '', + minTime: 'Moet minimum XYZ zijn', + invalid_time: 'Ongeldige tijd opgegeven', + }, + en: { + required: '', + maxTime: 'Must be maximum XYZ', + }, + }, + // registration tab + registration: { + attribute: '', + }, + // custom OF extensions + openForms: { + // translations tab in builder form + translations: { + nl: { + label: 'foo', + tooltip: 'bar', + }, + }, + }, +}); + +// invalid, multiple true and non-array default value +expectNotAssignable({ + id: 'ezftxdl', + type: 'time', + key: 'someTime', + label: 'Some time', + inputType: 'text', + format: 'HH:mm', + validateOn: 'blur', + multiple: true, + defaultValue: '', +} as const); + +// invalid, multiple false and array default value +expectNotAssignable({ + id: 'ezftxdl', + type: 'time', + key: 'someTime', + label: 'Some time', + inputType: 'text', + format: 'HH:mm', + validateOn: 'blur', + multiple: false, + defaultValue: [''], +} as const); + +// invalid, multiple true and wrong default value in array element +expectNotAssignable({ + id: 'ezftxdl', + type: 'time', + key: 'someTime', + label: 'Some time', + inputType: 'text', + format: 'HH:mm', + validateOn: 'blur', + multiple: true, + defaultValue: [new Date()], +} as const); diff --git a/tsconfig.json b/tsconfig.json index 56a7f93..7edab70 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "strictBindCallApply": true, "strictNullChecks": true, "allowSyntheticDefaultImports": true, + "noErrorTruncation": true, "paths": { "@/*": ["./*"], }