field.model.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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. Object.assign(this, meta);
  80. if (!this.source) {
  81. // If source is not supplied it's the same as the name
  82. this.source = this.name;
  83. }
  84. if (typeof this.label === 'undefined') {
  85. // If label is not supplied set it to the unCamelCased'n'Spaced name
  86. // e.g. supervisorCardNumber --> Supervisor Card Number
  87. this.label = unCamelCase(this.name);
  88. }
  89. if (typeof this.value === 'undefined') {
  90. this.value = this.default;
  91. }
  92. // TODO: Consider binding context to standard validators as well as async validators
  93. // BUT check this doesn't stop Angular's built-in validators from working (do they use 'this'?)
  94. if (typeof this.asyncValidators === 'function' || Array.isArray(this.asyncValidators)) {
  95. if (typeof this.asyncValidators === 'function') {
  96. const boundFn = this.asyncValidators.bind(context);
  97. this.asyncValidators = boundFn;
  98. } else {
  99. this.asyncValidators.forEach((valFn, i) => {
  100. const boundFn = valFn.bind(context);
  101. this.asyncValidators[i] = boundFn;
  102. });
  103. }
  104. }
  105. }
  106. }
  107. class Option implements IOption {
  108. // Can take a simple string value, a value-label pair [value, label],
  109. // or an Option of the form { label: string, value: string }
  110. label: string;
  111. value: string | number | boolean;
  112. constructor(opt: string | string[] | Option) {
  113. if (typeof opt === 'object') {
  114. if (Array.isArray(opt)) {
  115. this.label = opt[1];
  116. this.value = opt[0];
  117. } else {
  118. this.label = opt.label;
  119. this.value = opt.value;
  120. }
  121. } else {
  122. // Simple string
  123. this.label = opt;
  124. this.value = opt;
  125. }
  126. }
  127. }
  128. abstract class OptionsField extends SimpleField {
  129. options: Option[] | (() => Option[]) = [];
  130. constructor(meta: IOptionsFieldMetaData, context?: any) {
  131. super(meta);
  132. let options;
  133. if (typeof meta.options === 'function') {
  134. const boundFn = meta.options.bind(context);
  135. options = boundFn();
  136. } else {
  137. options = meta.options;
  138. }
  139. if (Array.isArray(options)) {
  140. this.options = options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
  141. } else {
  142. this.options = [
  143. new Option({ label: 'Yes', value: true }),
  144. new Option({ label: 'No', value: false })
  145. ];
  146. }
  147. }
  148. }
  149. // ---------------------------------------------------------------------------------------------------------------------
  150. // Concrete Implementations - Native Form Components
  151. class TextField extends SimpleField {
  152. type = 'Text';
  153. link?: ILink;
  154. }
  155. class TextareaField extends SimpleField {
  156. type = 'Textarea';
  157. }
  158. class PasswordField extends SimpleField {
  159. type = 'Password';
  160. }
  161. class SelectField extends OptionsField {
  162. type = 'Select';
  163. link?: ILink;
  164. constructor(meta: IOptionsFieldMetaData, context?: any) {
  165. super(meta, context);
  166. // If there's only one possible choice set the value
  167. if (this.options.length === 1) {
  168. this.default = this.options[0].value as string;
  169. }
  170. }
  171. }
  172. class RadioField extends OptionsField {
  173. type = 'Radio';
  174. }
  175. class CheckboxField extends SimpleField {
  176. type = 'Checkbox';
  177. isChecked: boolean;
  178. default: any = false;
  179. checkedValue: boolean|number|string = true;
  180. rowLabel: null;
  181. constructor(meta: ISimpleFieldMetaData) {
  182. super(meta);
  183. if (meta.default) {
  184. this.default = meta.default; // Necessary to ovveride defaut of true, since this cannot be done by super(meta)
  185. }
  186. if (meta.value) {
  187. this.checkedValue = meta.value;
  188. }
  189. if (meta.checkedValue) {
  190. this.checkedValue = meta.checkedValue; // Ditto
  191. }
  192. if (typeof meta.value === 'undefined') {
  193. this.value = this.default; // Get default from this class, not superclass
  194. }
  195. if (!meta.label) {
  196. meta.label = unCamelCase(this.checkedValue.toString());
  197. }
  198. }
  199. }
  200. class HiddenField extends SimpleField {
  201. type = 'Hidden';
  202. }
  203. // ---------------------------------------------------------------------------------------------------------------------
  204. // Concrete Implementations - Custom Form Components
  205. class CheckbuttonField extends CheckboxField {
  206. type = 'Checkbutton';
  207. }
  208. class DropdownModifiedInputField extends SimpleField {
  209. type = 'DropdownModifiedInput';
  210. modifiers: string[] = standardModifiers;
  211. transform: ValueTransformer = standardTransformer;
  212. constructor(meta: IDropdownModifiedInputFieldMetaData) {
  213. super(meta);
  214. }
  215. }
  216. class MultilineField extends SimpleField {
  217. type = 'Multiline';
  218. lines: number;
  219. maxLineLength: number;
  220. }
  221. // ---------------------------------------------------------------------------------------------------------------------
  222. // Concrete Implementations - Custom FormGroup Components (which render a group of FormControls)
  223. class CheckboxGroup {
  224. type = 'CheckboxGroup';
  225. name: string;
  226. label?: string;
  227. groupName: string;
  228. firstEnablesRest?: boolean;
  229. showAllOrNone?: boolean;
  230. meta: CheckbuttonField[] | { [key: string]: CheckbuttonField };
  231. constructor(groupmeta: any) {
  232. console.log(groupmeta);
  233. Object.assign(this, groupmeta);
  234. if (typeof this.label === 'undefined') {
  235. // If label is not supplied set it to the unCamelCased'n'Spaced name
  236. // e.g. supervisorCardNumber --> Supervisor Card Number
  237. this.label = unCamelCase(this.name);
  238. }
  239. // Can render as a FormArray or FormGroup depending on input data
  240. console.log('GMM', groupmeta.meta);
  241. if (Array.isArray(groupmeta.meta)) {
  242. const arrayMembers = groupmeta.meta;
  243. this.meta = arrayMembers.map(cb => new CheckbuttonField(cb));
  244. } else {
  245. const groupMembers = groupmeta.meta;
  246. this.meta = Object.entries(groupMembers)
  247. .map( ([key, cb]) => [key, new CheckbuttonField(cb as ISimpleFieldMetaData)] )
  248. .reduce((res, [key, cbf]) => { res[key as string] = cbf; return res; }, {});
  249. }
  250. }
  251. }
  252. class CheckbuttonGroup extends CheckboxGroup {
  253. type = 'CheckbuttonGroup';
  254. }
  255. // ---------------------------------------------------------------------------------------------------------------------
  256. // Concrete Implementations
  257. class DatetimeField extends SimpleField {
  258. type = 'Datetime';
  259. value: Date | string;
  260. constructor(meta) {
  261. super(meta);
  262. if (typeof this.value === 'string') {
  263. this.value = new Date(this.value);
  264. }
  265. if (!(this.value instanceof Date)) {
  266. this.value = new Date();
  267. }
  268. }
  269. }
  270. class DatepickerField extends SimpleField {
  271. type = 'Datepicker';
  272. value: Date = new Date();
  273. }
  274. // ---------------------------------------------------------------------------------------------------------------------
  275. // Repeating Fields
  276. class RepeatingField<T> {
  277. type = 'RepeatingField';
  278. name: string;
  279. label: string;
  280. seed: StringMap<any>;
  281. meta: T[]; // An array of fields of type T
  282. minRepeat: number = 1;
  283. maxRepeat: number = 10;
  284. initialRepeat: number;
  285. showAddControl: boolean = true;
  286. showDeleteControl: boolean = true;
  287. constructor(repeatingFieldMeta: StringMap<any>) {
  288. Object.assign(this, repeatingFieldMeta);
  289. if (typeof this.label === 'undefined') {
  290. this.label = unCamelCase(this.name);
  291. }
  292. }
  293. }
  294. // ---------------------------------------------------------------------------------------------------------------------
  295. // Containers
  296. class Container {
  297. type = 'Container';
  298. name: string;
  299. label = '';
  300. seed: StringMap<any>;
  301. template?: TemplateRef<any>;
  302. button: string; // IS THIS ACTUALLY USED?
  303. focussed: boolean = true; // IS THIS ACTUALLY USED?
  304. class?: string | string[];
  305. id?: string;
  306. meta: StringMap<any>; // TODO: Tighten up on type with union type
  307. validators: ValidatorFn[] = [];
  308. asyncValidators: AsyncValidatorFn[] = [];
  309. constructor(containerMeta: StringMap<any>) {
  310. Object.assign(this, containerMeta);
  311. if (typeof this.label === 'undefined') {
  312. this.label = unCamelCase(this.name);
  313. }
  314. }
  315. }
  316. class RepeatingContainer {
  317. type = 'RepeatingContainer';
  318. name: string;
  319. prefix: string = 'group';
  320. label = '';
  321. seed: StringMap<any>;
  322. template?: TemplateRef<any>;
  323. meta: Container[]; // An array of Containers
  324. minRepeat: number = 1;
  325. maxRepeat: number = 10;
  326. initialRepeat: number;
  327. showAddControl: boolean = true;
  328. showDeleteControl: boolean = true;
  329. primaryField: string = '';
  330. display: string = 'SINGLE'; // Display strategy to use - ALL or SINGLE - All at once, or one at a time (with a switcher)
  331. constructor(containerMeta: StringMap<any>) {
  332. Object.assign(this, containerMeta);
  333. if (typeof this.label === 'undefined') {
  334. this.label = unCamelCase(this.name);
  335. }
  336. if (!containerMeta.initialRepeat) {
  337. this.initialRepeat = containerMeta.meta.length;
  338. }
  339. this.meta = containerMeta.meta.map((m, i) => new Container({
  340. name: `${this.prefix}${i + 1}`,
  341. meta: m,
  342. button: unCamelCase(`${this.prefix}${i + 1}`),
  343. focussed: this.display === 'SINGLE' ? i === 0 : true
  344. }));
  345. }
  346. }
  347. // ---------------------------------------------------------------------------------------------------------------------
  348. // Button Group
  349. interface IButtonInterface {
  350. label: string;
  351. fnId: string;
  352. class?: string;
  353. icon?: string;
  354. }
  355. class Button implements IButtonInterface {
  356. label;
  357. fnId;
  358. class = 'btn-primary';
  359. constructor(buttonProps) {
  360. Object.assign(this, buttonProps);
  361. }
  362. }
  363. class ButtonGroup {
  364. type = 'ButtonGroup';
  365. name: string;
  366. label = '';
  367. meta: Button[];
  368. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  369. constructor(meta) {
  370. Object.assign(this, meta);
  371. this.meta = this.meta.map(b => b instanceof Button ? b : new Button(b));
  372. }
  373. }
  374. // ---------------------------------------------------------------------------------------------------------------------
  375. // Heading
  376. class Heading {
  377. type = 'Heading';
  378. text = 'Missing Heading Text';
  379. level = 3;
  380. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  381. readonly noLabel = true; // Indicates this has no label, so don't create normal form row
  382. constructor(meta) {
  383. Object.assign(this, meta);
  384. }
  385. }
  386. // ---------------------------------------------------------------------------------------------------------------------
  387. // Display - displays non-editable vaklues lie a text field (but no input)
  388. class DisplayField extends SimpleField {
  389. value: string;
  390. link?: ILink;
  391. readonly disabled = true;
  392. constructor(meta) {
  393. super(meta);
  394. }
  395. }
  396. // ---------------------------------------------------------------------------------------------------------------------
  397. // ---------------------------------------------------------------------------------------------------------------------
  398. // ---------------------------------------------------------------------------------------------------------------------
  399. // Exports
  400. // Interfaces
  401. export {
  402. IOption, ILink
  403. };
  404. // Classes
  405. export {
  406. SimpleField, Option,
  407. TextField, TextareaField, PasswordField, SelectField, RadioField, CheckboxField, HiddenField,
  408. CheckbuttonField, DropdownModifiedInputField, MultilineField,
  409. CheckboxGroup, CheckbuttonGroup,
  410. DatetimeField, DatepickerField,
  411. RepeatingField,
  412. Container, RepeatingContainer,
  413. ButtonGroup, Heading, DisplayField
  414. };