field.model.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. /* *********************************************************************************************************************
  2. * MetaData models for Form Fields
  3. * -------------------------------
  4. * Keep in one file for now, but maybe split if this grows too large
  5. ******************************************************************************************************************** */
  6. import { TemplateRef } from '@angular/core';
  7. import { ValidatorFn, AsyncValidatorFn } from '@angular/forms';
  8. import { ValueTransformer } from './../interfaces';
  9. import { standardModifiers, standardTransformer } from './../utils';
  10. interface ISimpleFieldMetaData {
  11. name: string; // The FormControl name
  12. type?: string; // The component type e.g. Text, Checkbutton, Timepicker, etc
  13. label?: string; // The field label - defaults to unCamelCased name if not supplied
  14. value?: any; // The field value - defaults to empty string if not supplied
  15. checkedValue?: boolean|number|string; // Checkboxes and Checkbuttons only
  16. default?: any; // Default value
  17. placeholder?: string; // Optional placeholder text
  18. class?: string | string[]; // CSS classes to apply
  19. id?: string; // CSS id to apply
  20. disabled?: boolean; // Whether field is initially disabled
  21. change?: string; // Name of function in host component to call when value changes
  22. source?: string; // Location in API-returned model - defaults to name
  23. before?: string; // Ordering instruction - move before <name of another key in group>
  24. after?: string; // Ordering instruction - move after <name of another key in group>
  25. validators?: ValidatorFn[]; // Array of validator functions - following Angular FormControl API
  26. asyncValidators?: AsyncValidatorFn[]; // Array of async validator functions - following Angular FormControl API
  27. valFailureMsgs?: StringMap<any>; // Validation failure messages - display appropriate message if validation fails
  28. }
  29. interface IOption {
  30. label: string;
  31. value: string | number | boolean;
  32. }
  33. interface IOptionsFieldMetaData extends ISimpleFieldMetaData {
  34. options: string[] | IOption[] | (() => IOption[]); // Array of Options - for select, radio-button-group and other 'multiple-choice' types
  35. horizontal?: boolean; // Whether to arrange radio buttons or checkboxes horizontally (default false)
  36. }
  37. // For components that include links to other pages
  38. interface ILink {
  39. label: string;
  40. route: any[] | string;
  41. }
  42. interface IDropdownModifiedInputFieldMetaData extends ISimpleFieldMetaData {
  43. modifiers: string[];
  44. transform: ValueTransformer;
  45. }
  46. interface ITimePickerFieldMetaData extends ISimpleFieldMetaData {
  47. value: Date | string;
  48. format: string;
  49. steps: StringMap<any>;
  50. }
  51. // Utility to unCamelCase
  52. const unCamelCase = (str: string): string => str.replace(/([A-Z])/g, ' $1')
  53. .replace(/(\w)(\d)/g, '$1 $2')
  54. .replace(/^./, s => s.toUpperCase())
  55. .replace(/([A-Z]) /g, '$1')
  56. // .replace(/ ([A-Z][a-z])/g, s => s.toLowerCase()) lowercase all but first word and acronyms
  57. ;
  58. // ---------------------------------------------------------------------------------------------------------------------
  59. // Form Field MetaData Models
  60. // ---------------------------------------------------------------------------------------------------------------------
  61. // Base Implementations
  62. abstract class SimpleField {
  63. type: string;
  64. name: string;
  65. source?: string;
  66. label?: string;
  67. value;
  68. checkedValue?: boolean|number|string;
  69. default = '';
  70. placeholder = '';
  71. class?: string | string[];
  72. id?: string;
  73. disabled = false;
  74. change?: string;
  75. validators: ValidatorFn|ValidatorFn[] = [];
  76. asyncValidators: AsyncValidatorFn|AsyncValidatorFn[] = [];
  77. valFailureMsgs: StringMap<any> = {};
  78. constructor(meta: ISimpleFieldMetaData, context?: any) {
  79. if (typeof meta !== 'object') {
  80. return;
  81. }
  82. Object.assign(this, meta);
  83. if (!this.source) {
  84. // If source is not supplied it's the same as the name
  85. this.source = this.name;
  86. }
  87. if (typeof this.label === 'undefined') {
  88. // If label is not supplied set it to the unCamelCased'n'Spaced name
  89. // e.g. supervisorCardNumber --> Supervisor Card Number
  90. this.label = unCamelCase(this.name);
  91. }
  92. if (typeof this.value === 'undefined') {
  93. this.value = this.default;
  94. }
  95. // TODO: Consider binding context to standard validators as well as async validators
  96. // BUT check this doesn't stop Angular's built-in validators from working (do they use 'this'?)
  97. if (typeof this.asyncValidators === 'function' || Array.isArray(this.asyncValidators)) {
  98. if (typeof this.asyncValidators === 'function') {
  99. const boundFn = this.asyncValidators.bind(context);
  100. this.asyncValidators = boundFn;
  101. } else {
  102. this.asyncValidators.forEach((valFn, i) => {
  103. const boundFn = valFn.bind(context);
  104. this.asyncValidators[i] = boundFn;
  105. });
  106. }
  107. }
  108. }
  109. }
  110. class Option implements IOption {
  111. // Can take a simple string value, a value-label pair [value, label],
  112. // or an Option of the form { label: string, value: string }
  113. label: string;
  114. value: string | number | boolean;
  115. constructor(opt: string | string[] | Option) {
  116. if (typeof opt === 'object') {
  117. if (Array.isArray(opt)) {
  118. this.label = opt[1];
  119. this.value = opt[0];
  120. } else {
  121. this.label = opt.label;
  122. this.value = opt.value;
  123. }
  124. } else {
  125. // Simple string
  126. this.label = opt;
  127. this.value = opt;
  128. }
  129. }
  130. }
  131. abstract class OptionsField extends SimpleField {
  132. options: Option[] | (() => Option[]) = [];
  133. constructor(meta: IOptionsFieldMetaData, context?: any) {
  134. super(meta);
  135. let options;
  136. if (typeof meta.options === 'function') {
  137. const boundFn = meta.options.bind(context);
  138. options = boundFn();
  139. } else {
  140. options = meta.options;
  141. }
  142. if (Array.isArray(options)) {
  143. this.options = options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
  144. } else {
  145. this.options = [
  146. new Option({ label: 'Yes', value: true }),
  147. new Option({ label: 'No', value: false })
  148. ];
  149. }
  150. }
  151. }
  152. // ---------------------------------------------------------------------------------------------------------------------
  153. // Concrete Implementations - Native Form Components
  154. class TextField extends SimpleField {
  155. type = 'Text';
  156. link?: ILink;
  157. }
  158. class TextareaField extends SimpleField {
  159. type = 'Textarea';
  160. }
  161. class PasswordField extends SimpleField {
  162. type = 'Password';
  163. }
  164. class SelectField extends OptionsField {
  165. type = 'Select';
  166. link?: ILink;
  167. constructor(meta: IOptionsFieldMetaData, context?: any) {
  168. super(meta, context);
  169. // If there's only one possible choice set the value
  170. if (this.options.length === 1) {
  171. this.default = this.options[0].value as string;
  172. }
  173. }
  174. }
  175. class RadioField extends OptionsField {
  176. type = 'Radio';
  177. }
  178. class CheckboxField extends SimpleField {
  179. type = 'Checkbox';
  180. isChecked: boolean;
  181. default: any = false;
  182. checkedValue: boolean|number|string = true;
  183. rowLabel: null;
  184. constructor(meta: ISimpleFieldMetaData) {
  185. super(meta);
  186. if (meta.default) {
  187. this.default = meta.default; // Necessary to ovveride defaut of true, since this cannot be done by super(meta)
  188. }
  189. if (meta.value) {
  190. this.checkedValue = meta.value;
  191. }
  192. if (meta.checkedValue) {
  193. this.checkedValue = meta.checkedValue; // Ditto
  194. }
  195. if (typeof meta.value === 'undefined') {
  196. this.value = this.default; // Get default from this class, not superclass
  197. }
  198. if (!meta.label) {
  199. this.label = unCamelCase(this.checkedValue.toString());
  200. }
  201. }
  202. }
  203. class HiddenField extends SimpleField {
  204. type = 'Hidden';
  205. }
  206. // ---------------------------------------------------------------------------------------------------------------------
  207. // Concrete Implementations - Custom Form Components
  208. class CheckbuttonField extends CheckboxField {
  209. type = 'Checkbutton';
  210. }
  211. class DropdownModifiedInputField extends SimpleField {
  212. type = 'DropdownModifiedInput';
  213. modifiers: string[] = standardModifiers;
  214. transform: ValueTransformer = standardTransformer;
  215. constructor(meta: IDropdownModifiedInputFieldMetaData) {
  216. super(meta);
  217. }
  218. }
  219. class MultilineField extends SimpleField {
  220. type = 'Multiline';
  221. lines: number;
  222. maxLineLength: number;
  223. }
  224. // ---------------------------------------------------------------------------------------------------------------------
  225. // Concrete Implementations - Custom FormGroup Components (which render a group of FormControls)
  226. class CheckboxGroup {
  227. type = 'CheckboxGroup';
  228. name: string;
  229. label?: string;
  230. groupName: string;
  231. firstEnablesRest?: boolean;
  232. showAllOrNone?: boolean;
  233. meta: CheckboxField[] | { [key: string]: CheckboxField };
  234. constructor(groupmeta: any) {
  235. Object.assign(this, groupmeta);
  236. if (typeof this.label === 'undefined') {
  237. // If label is not supplied set it to the unCamelCased'n'Spaced name
  238. // e.g. supervisorCardNumber --> Supervisor Card Number
  239. this.label = unCamelCase(this.name);
  240. }
  241. // Can render as a FormArray or FormGroup depending on input data
  242. if (Array.isArray(groupmeta.meta)) {
  243. const arrayMembers = groupmeta.meta;
  244. this.meta = arrayMembers.map(cb => new CheckboxField(cb));
  245. } else {
  246. const groupMembers = groupmeta.meta;
  247. this.meta = Object.entries(groupMembers)
  248. .map( ([key, cb]) => [key, new CheckboxField(cb as ISimpleFieldMetaData)] )
  249. .reduce((res, [key, cbf]) => { res[key as string] = cbf; return res; }, {});
  250. }
  251. }
  252. }
  253. class CheckbuttonGroup extends CheckboxGroup {
  254. type = 'CheckbuttonGroup';
  255. meta: CheckbuttonField[] | { [key: string]: CheckbuttonField };
  256. }
  257. // ---------------------------------------------------------------------------------------------------------------------
  258. // Concrete Implementations
  259. class DatetimeField extends SimpleField {
  260. type = 'Datetime';
  261. value: Date | string;
  262. constructor(meta) {
  263. super(meta);
  264. if (typeof this.value === 'string') {
  265. this.value = new Date(this.value);
  266. }
  267. if (!(this.value instanceof Date)) {
  268. this.value = new Date();
  269. }
  270. }
  271. }
  272. class DatepickerField extends SimpleField {
  273. type = 'Datepicker';
  274. value: Date = new Date();
  275. }
  276. // ---------------------------------------------------------------------------------------------------------------------
  277. // Repeating Fields
  278. class RepeatingField<T> {
  279. type = 'RepeatingField';
  280. name: string;
  281. label: string;
  282. seed: StringMap<any>;
  283. meta: T[]; // An array of fields of type T
  284. minRepeat: number = 1;
  285. maxRepeat: number = 10;
  286. initialRepeat: number = 1;
  287. showAddControl: boolean = true;
  288. showDeleteControl: boolean = true;
  289. addControlLabel: string = '';
  290. deleteControlLabel: string = '';
  291. constructor(repeatingFieldMeta: StringMap<any>) {
  292. Object.assign(this, repeatingFieldMeta);
  293. if (typeof this.label === 'undefined') {
  294. this.label = unCamelCase(this.name);
  295. }
  296. }
  297. }
  298. // ---------------------------------------------------------------------------------------------------------------------
  299. // Containers
  300. class Container {
  301. type = 'Container';
  302. name: string;
  303. label = '';
  304. seed: StringMap<any>;
  305. template?: TemplateRef<any>;
  306. button: string; // IS THIS ACTUALLY USED?
  307. focussed: boolean = true; // IS THIS ACTUALLY USED?
  308. class?: string | string[];
  309. id?: string;
  310. meta: StringMap<any>; // TODO: Tighten up on type with union type
  311. validators: ValidatorFn[] = [];
  312. asyncValidators: AsyncValidatorFn[] = [];
  313. constructor(containerMeta: StringMap<any>) {
  314. Object.assign(this, containerMeta);
  315. if (typeof this.label === 'undefined') {
  316. this.label = unCamelCase(this.name);
  317. }
  318. }
  319. }
  320. class RepeatingContainer {
  321. type = 'RepeatingContainer';
  322. name: string;
  323. prefix: string = 'group';
  324. label = '';
  325. seed: StringMap<any>;
  326. template?: TemplateRef<any>;
  327. meta: Container[]; // An array of Containers
  328. minRepeat: number = 1;
  329. maxRepeat: number = 10;
  330. initialRepeat: number = 1;
  331. showAddControl: boolean = true;
  332. showDeleteControl: boolean = true;
  333. addControlLabel: string = '';
  334. deleteControlLabel: string = '';
  335. primaryField: string = '';
  336. display: string = 'ALL'; // Display strategy to use - ALL or SINGLE - All at once, or one at a time (with a switcher)
  337. constructor(containerMeta: StringMap<any>) {
  338. Object.assign(this, containerMeta);
  339. if (typeof this.label === 'undefined') {
  340. this.label = unCamelCase(this.name);
  341. }
  342. if (!containerMeta.initialRepeat) {
  343. this.initialRepeat = containerMeta.meta.length;
  344. }
  345. this.meta = containerMeta.meta.map((m, i) => new Container({
  346. name: `${this.prefix}${i + 1}`,
  347. meta: m,
  348. button: unCamelCase(`${this.prefix}${i + 1}`),
  349. focussed: this.display === 'SINGLE' ? i === 0 : true
  350. }));
  351. }
  352. }
  353. // ---------------------------------------------------------------------------------------------------------------------
  354. // Button Group
  355. interface IButtonInterface {
  356. label: string;
  357. fnId: string;
  358. class?: string;
  359. icon?: string;
  360. }
  361. class Button implements IButtonInterface {
  362. label;
  363. fnId;
  364. class = 'btn-primary';
  365. constructor(buttonProps) {
  366. Object.assign(this, buttonProps);
  367. }
  368. }
  369. class ButtonGroup {
  370. type = 'ButtonGroup';
  371. name: string;
  372. label = '';
  373. meta: Button[];
  374. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  375. constructor(meta) {
  376. Object.assign(this, meta);
  377. this.meta = this.meta.map(b => b instanceof Button ? b : new Button(b));
  378. }
  379. }
  380. // ---------------------------------------------------------------------------------------------------------------------
  381. // Heading
  382. class Heading {
  383. type = 'Heading';
  384. text = 'Missing Heading Text';
  385. level = 3;
  386. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  387. readonly noLabel = true; // Indicates this has no label, so don't create normal form row
  388. constructor(meta) {
  389. Object.assign(this, meta);
  390. }
  391. }
  392. // ---------------------------------------------------------------------------------------------------------------------
  393. // Display - displays non-editable vaklues lie a text field (but no input)
  394. class DisplayField extends SimpleField {
  395. value: string;
  396. link?: ILink;
  397. readonly disabled = true;
  398. constructor(meta) {
  399. super(meta);
  400. }
  401. }
  402. // ---------------------------------------------------------------------------------------------------------------------
  403. // ---------------------------------------------------------------------------------------------------------------------
  404. // ---------------------------------------------------------------------------------------------------------------------
  405. // Exports
  406. // Interfaces
  407. export {
  408. IOption, ILink
  409. };
  410. // Classes
  411. export {
  412. SimpleField, Option,
  413. TextField, TextareaField, PasswordField, SelectField, RadioField, CheckboxField, HiddenField,
  414. CheckbuttonField, DropdownModifiedInputField, MultilineField,
  415. CheckboxGroup, CheckbuttonGroup,
  416. DatetimeField, DatepickerField,
  417. RepeatingField,
  418. Container, RepeatingContainer,
  419. ButtonGroup, Heading, DisplayField
  420. };