field.model.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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. source?: string; // Location in API-returned model - defaults to name
  13. type?: string; // The component type e.g. BasicInput, Checkbutton, Timepicker, etc
  14. label?: string; // The field label - defaults to unCamelCased name if not supplied
  15. value?: any; // The field value - defaults to empty string if not supplied
  16. checkedValue?: boolean|number|string; // Checkboxes and Checkbuttons only
  17. default?: any; // Default value
  18. placeholder?: string; // Optional placeholder text
  19. class?: string | string[]; // CSS classes to apply
  20. id?: string; // CSS id to apply
  21. before?: string; // Ordering instruction - move before <name of another key in group>
  22. after?: string; // Ordering instruction - move after <name of another key in group>
  23. disabled?: boolean; // Whether field is initially disabled
  24. validators?: ValidatorFn[]; // Array of validator functions - following Angular FormControl API
  25. asyncValidators?: AsyncValidatorFn[]; // Array of async validator functions - following Angular FormControl API
  26. valFailureMsgs?: StringMap<any>; // Validation failure messages - display appropriate message if validation fails
  27. onChange?: (val) => {}; // Function to call when field's value changes
  28. }
  29. interface IOption {
  30. label: string;
  31. value: string | number | boolean;
  32. }
  33. interface IOptionsFieldMetaData extends ISimpleFieldMetaData {
  34. options; // Array of Options - for select, radio-button-group and other 'multiple-choice' types
  35. horizontal?: boolean; // Whether to arrang 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. validators: ValidatorFn[] = [];
  75. asyncValidators: AsyncValidatorFn[] = [];
  76. valFailureMsgs: StringMap<any> = {};
  77. constructor(meta: ISimpleFieldMetaData) {
  78. Object.assign(this, meta);
  79. if (!this.source) {
  80. // If source is not supplied it's the same as the name
  81. this.source = this.name;
  82. }
  83. if (typeof this.label === 'undefined') {
  84. // If label is not supplied set it to the unCamelCased'n'Spaced name
  85. // e.g. supervisorCardNumber --> Supervisor Card Number
  86. this.label = unCamelCase(this.name);
  87. }
  88. }
  89. }
  90. class Option implements IOption {
  91. // Can take a simple string value, a value-label pair [value, label],
  92. // or an Option of the form { label: string, value: string }
  93. label: string;
  94. value: string | number | boolean;
  95. constructor(opt: string | string[] | Option) {
  96. if (typeof opt === 'object') {
  97. if (Array.isArray(opt)) {
  98. this.label = opt[1];
  99. this.value = opt[0];
  100. } else {
  101. this.label = opt.label;
  102. this.value = opt.value;
  103. }
  104. } else {
  105. this.label = opt;
  106. this.value = opt;
  107. }
  108. }
  109. }
  110. abstract class OptionsField extends SimpleField {
  111. options: Option[] = [];
  112. constructor(meta: IOptionsFieldMetaData) {
  113. super(meta);
  114. let options = typeof meta.options === 'function' ? meta.options() : meta.options;
  115. if (Array.isArray(options)) {
  116. this.options = options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
  117. } else {
  118. this.options = [
  119. new Option({ label: 'Yes', value: true }),
  120. new Option({ label: 'No', value: false })
  121. ];
  122. }
  123. }
  124. }
  125. // ---------------------------------------------------------------------------------------------------------------------
  126. // Concrete Implementations - Native Form Components
  127. class TextField extends SimpleField {
  128. type = 'Text';
  129. link?: ILink;
  130. }
  131. class TextareaField extends SimpleField {
  132. type = 'Textarea';
  133. }
  134. class PasswordField extends SimpleField {
  135. type = 'Password';
  136. }
  137. class SelectField extends OptionsField {
  138. type = 'Select';
  139. link?: ILink;
  140. }
  141. class RadioField extends OptionsField {
  142. type = 'Radio';
  143. }
  144. class CheckboxField extends SimpleField {
  145. type = 'Checkbox';
  146. isChecked: boolean;
  147. default: any = false;
  148. checkedValue: boolean|number|string = true;
  149. rowLabel: null;
  150. constructor(meta: ISimpleFieldMetaData) {
  151. super(meta);
  152. if (meta.checkedValue) {
  153. this.checkedValue = meta.checkedValue;
  154. }
  155. if (meta.default) {
  156. this.default = meta.default;
  157. }
  158. }
  159. }
  160. class HiddenField extends SimpleField {
  161. type = 'Hidden';
  162. }
  163. // ---------------------------------------------------------------------------------------------------------------------
  164. // Concrete Implementations - Custom Form Components
  165. class CheckbuttonField extends CheckboxField {
  166. type = 'Checkbutton';
  167. }
  168. class DropdownModifiedInputField extends SimpleField {
  169. type = 'DropdownModifiedInput';
  170. modifiers: string[] = standardModifiers;
  171. transform: ValueTransformer = standardTransformer;
  172. constructor(meta: IDropdownModifiedInputFieldMetaData) {
  173. super(meta);
  174. }
  175. }
  176. class MultilineField extends SimpleField {
  177. type = 'Multiline';
  178. lines: number;
  179. maxLineLength: number;
  180. }
  181. // ---------------------------------------------------------------------------------------------------------------------
  182. // Concrete Implementations - Custom FormGroup Components (which render a group of FormControls)
  183. class CheckboxGroup {
  184. type = 'CheckboxGroup';
  185. name: string;
  186. label?: string;
  187. groupName: string;
  188. firstEnablesRest?: boolean;
  189. showAllOrNone?: boolean;
  190. meta: CheckbuttonField[] | { [key: string]: CheckbuttonField };
  191. constructor(groupmeta: any) {
  192. Object.assign(this, groupmeta);
  193. if (typeof this.label === 'undefined') {
  194. // If label is not supplied set it to the unCamelCased'n'Spaced name
  195. // e.g. supervisorCardNumber --> Supervisor Card Number
  196. this.label = unCamelCase(this.name);
  197. }
  198. // Can render as a FormArray or FormGroup depending on input data
  199. if (Array.isArray(groupmeta.meta)) {
  200. const arrayMembers = groupmeta.meta;
  201. this.meta = arrayMembers.map(cb => new CheckbuttonField(cb));
  202. } else {
  203. const groupMembers = groupmeta.meta;
  204. this.meta = Object.entries(groupMembers)
  205. .map( ([key, cb]) => [key, new CheckbuttonField(cb as ISimpleFieldMetaData)] )
  206. .reduce((res, [key, cbf]) => { res[key as string] = cbf; return res; }, {});
  207. }
  208. }
  209. }
  210. class CheckbuttonGroup extends CheckboxGroup {
  211. type = 'CheckbuttonGroup';
  212. }
  213. // ---------------------------------------------------------------------------------------------------------------------
  214. // Concrete Implementations - Kendo Form Components
  215. class TimepickerField extends SimpleField {
  216. type = 'Timepicker';
  217. value: Date | string;
  218. format = 'hh:mm a';
  219. steps = { hour: 1, minute: 15 };
  220. constructor(meta) {
  221. super(meta);
  222. if (typeof this.value === 'string') {
  223. const [hh, mm, ss] = this.value.split(':');
  224. this.value = new Date(2000, 6, 1, +hh || 0, +mm || 0, +ss || 0);
  225. }
  226. if (!(this.value instanceof Date)) {
  227. this.value = new Date();
  228. }
  229. }
  230. }
  231. class ClrDatepickerField extends SimpleField {
  232. type = 'ClrDatepicker';
  233. value: Date = new Date();
  234. }
  235. // ---------------------------------------------------------------------------------------------------------------------
  236. // Containers
  237. class Container {
  238. type = 'Container';
  239. name: string;
  240. label = '';
  241. seed: StringMap<any>;
  242. template?: TemplateRef<any>;
  243. button: string;
  244. focussed: boolean = true;
  245. meta: StringMap<any>; // TODO: Tighten up on type with union type
  246. constructor(containerMeta: StringMap<any>) {
  247. Object.assign(this, containerMeta);
  248. if (typeof this.label === 'undefined') {
  249. this.label = unCamelCase(this.name);
  250. }
  251. }
  252. }
  253. class RepeatingContainer {
  254. type = 'RepeatingContainer';
  255. name: string;
  256. prefix: string = 'group';
  257. label = '';
  258. seed: StringMap<any>;
  259. template?: TemplateRef<any>;
  260. meta: Container[]; // An array of Containers
  261. minRepeat: number = 1;
  262. maxRepeat: number = 10;
  263. initialRepeat: number;
  264. showAddControl: boolean = true;
  265. showDeleteControl: boolean = true;
  266. primaryField: string = '';
  267. display: string = 'SINGLE'; // Display strategy to use - ALL or SINGLE - All at once, or one at a time (with a switcher)
  268. constructor(containerMeta: StringMap<any>) {
  269. Object.assign(this, containerMeta);
  270. if (typeof this.label === 'undefined') {
  271. this.label = unCamelCase(this.name);
  272. }
  273. if (!containerMeta.initialRepeat) {
  274. this.initialRepeat = containerMeta.meta.length;
  275. }
  276. this.meta = containerMeta.meta.map((m, i) => new Container({
  277. name: `${this.prefix}${i+1}`,
  278. meta: m,
  279. button: unCamelCase(`${this.prefix}${i+1}`),
  280. focussed: this.display === 'SINGLE' ? i === 0 : true
  281. }));
  282. }
  283. }
  284. // ---------------------------------------------------------------------------------------------------------------------
  285. // Button Group
  286. interface IButtonInterface {
  287. label: string;
  288. fnId: string;
  289. class?: string;
  290. icon?: string;
  291. }
  292. class Button implements IButtonInterface {
  293. label;
  294. fnId;
  295. class = 'btn-primary';
  296. constructor(buttonProps) {
  297. Object.assign(this, buttonProps);
  298. }
  299. }
  300. class ButtonGroup {
  301. type = 'ButtonGroup';
  302. name: string;
  303. label = '';
  304. meta: Button[];
  305. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  306. constructor(meta) {
  307. Object.assign(this, meta);
  308. this.meta = this.meta.map(b => b instanceof Button ? b : new Button(b));
  309. }
  310. }
  311. // ---------------------------------------------------------------------------------------------------------------------
  312. // Heading
  313. class Heading {
  314. type = 'Heading';
  315. text = 'Missing Heading Text';
  316. level = 3;
  317. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  318. readonly noLabel = true; // Indicates this has no label, so don't create normal form row
  319. constructor(meta) {
  320. Object.assign(this, meta);
  321. }
  322. }
  323. // ---------------------------------------------------------------------------------------------------------------------
  324. // Display
  325. class DisplayField {
  326. value: string;
  327. link?: ILink;
  328. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  329. constructor(meta) {
  330. Object.assign(this, meta);
  331. }
  332. }
  333. // ---------------------------------------------------------------------------------------------------------------------
  334. // ---------------------------------------------------------------------------------------------------------------------
  335. // ---------------------------------------------------------------------------------------------------------------------
  336. // Exports
  337. export {
  338. SimpleField,
  339. TextField, TextareaField, PasswordField, SelectField, RadioField, CheckboxField, HiddenField,
  340. CheckbuttonField, DropdownModifiedInputField, MultilineField,
  341. CheckboxGroup, CheckbuttonGroup,
  342. TimepickerField, ClrDatepickerField,
  343. Container, RepeatingContainer,
  344. ButtonGroup, Heading, DisplayField
  345. };