Skip to content

Commit

Permalink
Merge pull request #14 from open-formulieren/feature/2-more-component…
Browse files Browse the repository at this point in the history
…-types

Implement more component types
  • Loading branch information
sergei-maertens authored Oct 16, 2023
2 parents da5b84d + b12fef2 commit 4b57b65
Show file tree
Hide file tree
Showing 10 changed files with 652 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/formio/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions src/formio/components/phonenumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {InputComponentSchema, MultipleCapable} from '..';

type Validator = 'required' | 'pattern';
type TranslatableKeys = 'label' | 'description' | 'tooltip';

export type PhoneNumberInputSchema = InputComponentSchema<string, Validator, TranslatableKeys>;

/**
* @group Form.io components
* @category Base types
*/
export interface BasePhoneNumberComponentSchema extends Omit<PhoneNumberInputSchema, 'hideLabel'> {
type: 'phoneNumber';
inputMask: null;
// additional properties
autocomplete?: string;
}

/**
* @group Form.io components
* @category Concrete types
*/
export type PhoneNumberComponentSchema = MultipleCapable<BasePhoneNumberComponentSchema>;
43 changes: 43 additions & 0 deletions src/formio/components/postcode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {InputComponentSchema, MultipleCapable, PrefillConfig} from '..';

type Validator = 'required' | 'pattern' | 'customMessage';
type TranslatableKeys = 'label' | 'description' | 'tooltip';

export type PostCodeInputSchema = InputComponentSchema<string, Validator, TranslatableKeys>;

/**
* 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<PostCodeInputSchema, 'hideLabel' | 'placeholder'> &
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<BasePostcodeComponentSchema>;
31 changes: 31 additions & 0 deletions src/formio/components/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {InputComponentSchema, MultipleCapable} from '..';

type Validator = 'required' | 'minTime' | 'maxTime';
type TranslatableKeys = 'label' | 'description' | 'tooltip';

export type TimeInputSchema = InputComponentSchema<string, Validator, TranslatableKeys>;

/**
* @group Form.io components
* @category Base types
*/
export interface BaseTimeComponentSchema extends Omit<TimeInputSchema, 'hideLabel'> {
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<BaseTimeComponentSchema>;
8 changes: 8 additions & 0 deletions src/formio/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {
ContentComponentSchema,
DateComponentSchema,
DateTimeComponentSchema,
EmailComponentSchema,
NumberComponentSchema,
PhoneNumberComponentSchema,
PostcodeComponentSchema,
TextFieldComponentSchema,
TimeComponentSchema,
} from './components';

/**
Expand Down Expand Up @@ -34,6 +38,10 @@ export type AnyComponentSchema =
| TextFieldComponentSchema
| EmailComponentSchema
| DateComponentSchema
| DateTimeComponentSchema
| TimeComponentSchema
| PhoneNumberComponentSchema
| PostcodeComponentSchema
| NumberComponentSchema
// layout
| ContentComponentSchema;
Expand Down
16 changes: 15 additions & 1 deletion src/formio/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand All @@ -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<Keys extends BaseErrorKeys = BaseErrorKeys> = {
[K in Keys]?: string;
Expand Down Expand Up @@ -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',
Expand All @@ -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
Expand Down
164 changes: 164 additions & 0 deletions test-d/formio/components/phonenumber.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import {expectAssignable, expectNotAssignable} from 'tsd';

import {PhoneNumberComponentSchema} from '../../../lib/';

// minimal textfield component schema
expectAssignable<PhoneNumberComponentSchema>({
id: 'yejak',
type: 'phoneNumber',
key: 'someInput',
label: 'Some input',
inputMask: null,
});

// with additional, phonenumber-component specific properties
expectAssignable<PhoneNumberComponentSchema>({
id: 'yejak',
type: 'phoneNumber',
key: 'someInput',
label: 'Some input',
inputMask: null,
placeholder: 'tel',
});

// multiple false and appropriate default value type
expectAssignable<PhoneNumberComponentSchema>({
id: 'yejak',
type: 'phoneNumber',
key: 'someInput',
label: 'Some input',
inputMask: null,
multiple: false,
defaultValue: '',
});
// multiple true and appropriate default value type
expectAssignable<PhoneNumberComponentSchema>({
id: 'yejak',
type: 'phoneNumber',
key: 'someInput',
label: 'Some input',
inputMask: null,
multiple: true,
defaultValue: [''],
});

// full, correct schema
expectAssignable<PhoneNumberComponentSchema>({
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<PhoneNumberComponentSchema>({
id: 'yejak',
type: 'textfield',
key: 'someInput',
label: 'Some input',
inputMask: null,
} as const);

// using unsupported properties
expectNotAssignable<PhoneNumberComponentSchema>({
id: 'yejak',
type: 'phoneNumber',
key: 'someInput',
label: 'Some input',
inputMask: null,
showCharCount: true,
} as const);

// incorrect, invalid validator key
expectNotAssignable<PhoneNumberComponentSchema>({
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<PhoneNumberComponentSchema>({
id: 'yejak',
type: 'phoneNumber',
key: 'someInput',
label: 'Some input',
inputMask: null,
multiple: true,
defaultValue: '',
} as const);

// invalid, multiple false and array default value
expectNotAssignable<PhoneNumberComponentSchema>({
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<PhoneNumberComponentSchema>({
id: 'yejak',
type: 'phoneNumber',
key: 'someInput',
label: 'Some input',
inputMask: null,
multiple: true,
defaultValue: [123],
} as const);
Loading

0 comments on commit 4b57b65

Please sign in to comment.