/* ********************************************************************************************************************* * MetaData models for Form Fields * ------------------------------- * Keep in one file for now, but maybe split if this grows too large ******************************************************************************************************************** */ import { TemplateRef } from '@angular/core'; import { ValidatorFn, AsyncValidatorFn } from '@angular/forms'; import { ValueTransformer } from './../interfaces'; import { standardModifiers, standardTransformer } from './../utils'; interface ISimpleFieldMetaData { name: string; // The FormControl name type?: string; // The component type e.g. Text, Checkbutton, Timepicker, etc label?: string; // The field label - defaults to unCamelCased name if not supplied value?: any; // The field value - defaults to empty string if not supplied checkedValue?: boolean|number|string; // Checkboxes and Checkbuttons only default?: any; // Default value placeholder?: string; // Optional placeholder text class?: string | string[]; // CSS classes to apply id?: string; // CSS id to apply disabled?: boolean; // Whether field is initially disabled change?: string; // Name of function in host component to call when value changes source?: string; // Location in API-returned model - defaults to name before?: string; // Ordering instruction - move before after?: string; // Ordering instruction - move after validators?: ValidatorFn[]; // Array of validator functions - following Angular FormControl API asyncValidators?: AsyncValidatorFn[]; // Array of async validator functions - following Angular FormControl API valFailureMsgs?: StringMap; // Validation failure messages - display appropriate message if validation fails } interface IOption { label: string; value: string | number | boolean; } interface IOptionsFieldMetaData extends ISimpleFieldMetaData { options: string[] | IOption[] | (() => IOption[]); // Array of Options - for select, radio-button-group and other 'multiple-choice' types horizontal?: boolean; // Whether to arrange radio buttons or checkboxes horizontally (default false) } // For components that include links to other pages interface ILink { label: string; route: any[] | string; } interface IDropdownModifiedInputFieldMetaData extends ISimpleFieldMetaData { modifiers: string[]; transform: ValueTransformer; } interface ITimePickerFieldMetaData extends ISimpleFieldMetaData { value: Date | string; format: string; steps: StringMap; } // Utility to unCamelCase const unCamelCase = (str: string): string => str.replace(/([A-Z])/g, ' $1') .replace(/(\w)(\d)/g, '$1 $2') .replace(/^./, s => s.toUpperCase()) .replace(/([A-Z]) /g, '$1') // .replace(/ ([A-Z][a-z])/g, s => s.toLowerCase()) lowercase all but first word and acronyms ; // --------------------------------------------------------------------------------------------------------------------- // Form Field MetaData Models // --------------------------------------------------------------------------------------------------------------------- // Base Implementations abstract class SimpleField { type: string; name: string; source?: string; label?: string; value; checkedValue?: boolean|number|string; default = ''; placeholder = ''; class?: string | string[]; id?: string; disabled = false; change?: string; validators: ValidatorFn|ValidatorFn[] = []; asyncValidators: AsyncValidatorFn|AsyncValidatorFn[] = []; valFailureMsgs: StringMap = {}; constructor(meta: ISimpleFieldMetaData, context?: any) { if (typeof meta !== 'object') { return; } Object.assign(this, meta); if (!this.source) { // If source is not supplied it's the same as the name this.source = this.name; } if (typeof this.label === 'undefined') { // If label is not supplied set it to the unCamelCased'n'Spaced name // e.g. supervisorCardNumber --> Supervisor Card Number this.label = unCamelCase(this.name); } if (typeof this.value === 'undefined') { this.value = this.default; } // TODO: Consider binding context to standard validators as well as async validators // BUT check this doesn't stop Angular's built-in validators from working (do they use 'this'?) if (typeof this.asyncValidators === 'function' || Array.isArray(this.asyncValidators)) { if (typeof this.asyncValidators === 'function') { const boundFn = this.asyncValidators.bind(context); this.asyncValidators = boundFn; } else { this.asyncValidators.forEach((valFn, i) => { const boundFn = valFn.bind(context); this.asyncValidators[i] = boundFn; }); } } } } class Option implements IOption { // Can take a simple string value, a value-label pair [value, label], // or an Option of the form { label: string, value: string } label: string; value: string | number | boolean; constructor(opt: string | string[] | Option) { if (typeof opt === 'object') { if (Array.isArray(opt)) { this.label = opt[1]; this.value = opt[0]; } else { this.label = opt.label; this.value = opt.value; } } else { // Simple string this.label = opt; this.value = opt; } } } abstract class OptionsField extends SimpleField { options: Option[] | (() => Option[]) = []; constructor(meta: IOptionsFieldMetaData, context?: any) { super(meta); let options; if (typeof meta.options === 'function') { const boundFn = meta.options.bind(context); options = boundFn(); } else { options = meta.options; } if (Array.isArray(options)) { this.options = options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []); } else { this.options = [ new Option({ label: 'Yes', value: true }), new Option({ label: 'No', value: false }) ]; } } } // --------------------------------------------------------------------------------------------------------------------- // Concrete Implementations - Native Form Components class TextField extends SimpleField { type = 'Text'; link?: ILink; } class TextareaField extends SimpleField { type = 'Textarea'; } class PasswordField extends SimpleField { type = 'Password'; } class SelectField extends OptionsField { type = 'Select'; link?: ILink; constructor(meta: IOptionsFieldMetaData, context?: any) { super(meta, context); // If there's only one possible choice set the value if (this.options.length === 1) { this.default = this.options[0].value as string; } } } class RadioField extends OptionsField { type = 'Radio'; } class CheckboxField extends SimpleField { type = 'Checkbox'; isChecked: boolean; default: any = false; checkedValue: boolean|number|string = true; rowLabel: null; constructor(meta: ISimpleFieldMetaData) { super(meta); if (meta.default) { this.default = meta.default; // Necessary to ovveride defaut of true, since this cannot be done by super(meta) } if (meta.value) { this.checkedValue = meta.value; } if (meta.checkedValue) { this.checkedValue = meta.checkedValue; // Ditto } if (typeof meta.value === 'undefined') { this.value = this.default; // Get default from this class, not superclass } if (!meta.label) { this.label = unCamelCase(this.checkedValue.toString()); } } } class HiddenField extends SimpleField { type = 'Hidden'; } // --------------------------------------------------------------------------------------------------------------------- // Concrete Implementations - Custom Form Components class CheckbuttonField extends CheckboxField { type = 'Checkbutton'; } class DropdownModifiedInputField extends SimpleField { type = 'DropdownModifiedInput'; modifiers: string[] = standardModifiers; transform: ValueTransformer = standardTransformer; constructor(meta: IDropdownModifiedInputFieldMetaData) { super(meta); } } class MultilineField extends SimpleField { type = 'Multiline'; lines: number; maxLineLength: number; } // --------------------------------------------------------------------------------------------------------------------- // Concrete Implementations - Custom FormGroup Components (which render a group of FormControls) class CheckboxGroup { type = 'CheckboxGroup'; name: string; label?: string; groupName: string; firstEnablesRest?: boolean; showAllOrNone?: boolean; meta: CheckboxField[] | { [key: string]: CheckboxField }; constructor(groupmeta: any) { Object.assign(this, groupmeta); if (typeof this.label === 'undefined') { // If label is not supplied set it to the unCamelCased'n'Spaced name // e.g. supervisorCardNumber --> Supervisor Card Number this.label = unCamelCase(this.name); } // Can render as a FormArray or FormGroup depending on input data if (Array.isArray(groupmeta.meta)) { const arrayMembers = groupmeta.meta; this.meta = arrayMembers.map(cb => new CheckboxField(cb)); } else { const groupMembers = groupmeta.meta; this.meta = Object.entries(groupMembers) .map( ([key, cb]) => [key, new CheckboxField(cb as ISimpleFieldMetaData)] ) .reduce((res, [key, cbf]) => { res[key as string] = cbf; return res; }, {}); } } } class CheckbuttonGroup extends CheckboxGroup { type = 'CheckbuttonGroup'; meta: CheckbuttonField[] | { [key: string]: CheckbuttonField }; } // --------------------------------------------------------------------------------------------------------------------- // Concrete Implementations class DatetimeField extends SimpleField { type = 'Datetime'; value: Date | string; constructor(meta) { super(meta); if (typeof this.value === 'string') { this.value = new Date(this.value); } if (!(this.value instanceof Date)) { this.value = new Date(); } } } class DatepickerField extends SimpleField { type = 'Datepicker'; value: Date = new Date(); } // --------------------------------------------------------------------------------------------------------------------- // Repeating Fields class RepeatingField { type = 'RepeatingField'; name: string; label: string; seed: StringMap; meta: T[]; // An array of fields of type T minRepeat: number = 1; maxRepeat: number = 10; initialRepeat: number = 1; showAddControl: boolean = true; showDeleteControl: boolean = true; addControlLabel: string = ''; deleteControlLabel: string = ''; constructor(repeatingFieldMeta: StringMap) { Object.assign(this, repeatingFieldMeta); if (typeof this.label === 'undefined') { this.label = unCamelCase(this.name); } } } // --------------------------------------------------------------------------------------------------------------------- // Containers class Container { type = 'Container'; name: string; label = ''; seed: StringMap; template?: TemplateRef; button: string; // IS THIS ACTUALLY USED? focussed: boolean = true; // IS THIS ACTUALLY USED? class?: string | string[]; id?: string; meta: StringMap; // TODO: Tighten up on type with union type validators: ValidatorFn[] = []; asyncValidators: AsyncValidatorFn[] = []; constructor(containerMeta: StringMap) { Object.assign(this, containerMeta); if (typeof this.label === 'undefined') { this.label = unCamelCase(this.name); } } } class RepeatingContainer { type = 'RepeatingContainer'; name: string; prefix: string = 'group'; label = ''; seed: StringMap; template?: TemplateRef; meta: Container[]; // An array of Containers minRepeat: number = 1; maxRepeat: number = 10; initialRepeat: number = 1; showAddControl: boolean = true; showDeleteControl: boolean = true; addControlLabel: string = ''; deleteControlLabel: string = ''; primaryField: string = ''; display: string = 'ALL'; // Display strategy to use - ALL or SINGLE - All at once, or one at a time (with a switcher) constructor(containerMeta: StringMap) { Object.assign(this, containerMeta); if (typeof this.label === 'undefined') { this.label = unCamelCase(this.name); } if (!containerMeta.initialRepeat) { this.initialRepeat = containerMeta.meta.length; } this.meta = containerMeta.meta.map((m, i) => new Container({ name: `${this.prefix}${i + 1}`, meta: m, button: unCamelCase(`${this.prefix}${i + 1}`), focussed: this.display === 'SINGLE' ? i === 0 : true })); } } // --------------------------------------------------------------------------------------------------------------------- // Button Group interface IButtonInterface { label: string; fnId: string; class?: string; icon?: string; } class Button implements IButtonInterface { label; fnId; class = 'btn-primary'; constructor(buttonProps) { Object.assign(this, buttonProps); } } class ButtonGroup { type = 'ButtonGroup'; name: string; label = ''; meta: Button[]; readonly noFormControls = true; // Indicates this has no FormControls associated with it constructor(meta) { Object.assign(this, meta); this.meta = this.meta.map(b => b instanceof Button ? b : new Button(b)); } } // --------------------------------------------------------------------------------------------------------------------- // Heading class Heading { type = 'Heading'; text = 'Missing Heading Text'; level = 3; readonly noFormControls = true; // Indicates this has no FormControls associated with it readonly noLabel = true; // Indicates this has no label, so don't create normal form row constructor(meta) { Object.assign(this, meta); } } // --------------------------------------------------------------------------------------------------------------------- // Display - displays non-editable vaklues lie a text field (but no input) class DisplayField extends SimpleField { value: string; link?: ILink; readonly disabled = true; constructor(meta) { super(meta); } } // --------------------------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------------------- // Exports // Interfaces export { IOption, ILink }; // Classes export { SimpleField, Option, TextField, TextareaField, PasswordField, SelectField, RadioField, CheckboxField, HiddenField, CheckbuttonField, DropdownModifiedInputField, MultilineField, CheckboxGroup, CheckbuttonGroup, DatetimeField, DatepickerField, RepeatingField, Container, RepeatingContainer, ButtonGroup, Heading, DisplayField };