123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470 |
- /* *********************************************************************************************************************
- * 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 <name of another key in group>
- after?: string; // Ordering instruction - move after <name of another key in group>
- validators?: ValidatorFn[]; // Array of validator functions - following Angular FormControl API
- asyncValidators?: AsyncValidatorFn[]; // Array of async validator functions - following Angular FormControl API
- valFailureMsgs?: StringMap<any>; // 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<any>;
- }
- // 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<any> = {};
- 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<T> {
- type = 'RepeatingField';
- name: string;
- label: string;
- seed: StringMap<any>;
- 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<any>) {
- Object.assign(this, repeatingFieldMeta);
- if (typeof this.label === 'undefined') {
- this.label = unCamelCase(this.name);
- }
- }
- }
- // ---------------------------------------------------------------------------------------------------------------------
- // Containers
- class Container {
- type = 'Container';
- name: string;
- label = '';
- seed: StringMap<any>;
- template?: TemplateRef<any>;
- button: string; // IS THIS ACTUALLY USED?
- focussed: boolean = true; // IS THIS ACTUALLY USED?
- class?: string | string[];
- id?: string;
- meta: StringMap<any>; // TODO: Tighten up on type with union type
- validators: ValidatorFn[] = [];
- asyncValidators: AsyncValidatorFn[] = [];
- constructor(containerMeta: StringMap<any>) {
- 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<any>;
- template?: TemplateRef<any>;
- 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<any>) {
- 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
- };
|