field.model.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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. // onChange?: (val) => {}; // Function to call when field's value changes
  29. }
  30. interface IOption {
  31. label: string;
  32. value: string | number | boolean;
  33. }
  34. interface IOptionsFieldMetaData extends ISimpleFieldMetaData {
  35. options; // Array of Options - for select, radio-button-group and other 'multiple-choice' types
  36. horizontal?: boolean; // Whether to arrang radio buttons or checkboxes horizontally (default false)
  37. }
  38. // For components that include links to other pages
  39. interface ILink {
  40. label: string;
  41. route: any[] | string;
  42. }
  43. interface IDropdownModifiedInputFieldMetaData extends ISimpleFieldMetaData {
  44. modifiers: string[];
  45. transform: ValueTransformer;
  46. }
  47. interface ITimePickerFieldMetaData extends ISimpleFieldMetaData {
  48. value: Date | string;
  49. format: string;
  50. steps: StringMap<any>;
  51. }
  52. // Utility to unCamelCase
  53. const unCamelCase = (str: string): string => str.replace(/([A-Z])/g, ' $1')
  54. .replace(/(\w)(\d)/g, '$1 $2')
  55. .replace(/^./, s => s.toUpperCase())
  56. .replace(/([A-Z]) /g, '$1')
  57. // .replace(/ ([A-Z][a-z])/g, s => s.toLowerCase()) lowercase all but first word and acronyms
  58. ;
  59. // ---------------------------------------------------------------------------------------------------------------------
  60. // Form Field MetaData Models
  61. // ---------------------------------------------------------------------------------------------------------------------
  62. // Base Implementations
  63. abstract class SimpleField {
  64. type: string;
  65. name: string;
  66. source?: string;
  67. label?: string;
  68. value;
  69. checkedValue?: boolean|number|string;
  70. default = '';
  71. placeholder = '';
  72. class?: string | string[];
  73. id?: string;
  74. disabled = false;
  75. change?: string;
  76. validators: ValidatorFn[] = [];
  77. asyncValidators: AsyncValidatorFn[] = [];
  78. valFailureMsgs: StringMap<any> = {};
  79. constructor(meta: ISimpleFieldMetaData) {
  80. Object.assign(this, meta);
  81. if (!this.source) {
  82. // If source is not supplied it's the same as the name
  83. this.source = this.name;
  84. }
  85. if (typeof this.label === 'undefined') {
  86. // If label is not supplied set it to the unCamelCased'n'Spaced name
  87. // e.g. supervisorCardNumber --> Supervisor Card Number
  88. this.label = unCamelCase(this.name);
  89. }
  90. }
  91. }
  92. class Option implements IOption {
  93. // Can take a simple string value, a value-label pair [value, label],
  94. // or an Option of the form { label: string, value: string }
  95. label: string;
  96. value: string | number | boolean;
  97. constructor(opt: string | string[] | Option) {
  98. if (typeof opt === 'object') {
  99. if (Array.isArray(opt)) {
  100. this.label = opt[1];
  101. this.value = opt[0];
  102. } else {
  103. this.label = opt.label;
  104. this.value = opt.value;
  105. }
  106. } else {
  107. this.label = opt;
  108. this.value = opt;
  109. }
  110. }
  111. }
  112. abstract class OptionsField extends SimpleField {
  113. options: Option[] = [];
  114. constructor(meta: IOptionsFieldMetaData, context?: any) {
  115. super(meta);
  116. let options;
  117. if (typeof meta.options === 'function') {
  118. const boundFn = meta.options.bind(context);
  119. options = boundFn();
  120. } else {
  121. options = meta.options;
  122. }
  123. if (Array.isArray(options)) {
  124. this.options = options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
  125. } else {
  126. this.options = [
  127. new Option({ label: 'Yes', value: true }),
  128. new Option({ label: 'No', value: false })
  129. ];
  130. }
  131. }
  132. }
  133. // ---------------------------------------------------------------------------------------------------------------------
  134. // Concrete Implementations - Native Form Components
  135. class TextField extends SimpleField {
  136. type = 'Text';
  137. link?: ILink;
  138. }
  139. class TextareaField extends SimpleField {
  140. type = 'Textarea';
  141. }
  142. class PasswordField extends SimpleField {
  143. type = 'Password';
  144. }
  145. class SelectField extends OptionsField {
  146. type = 'Select';
  147. link?: ILink;
  148. }
  149. class RadioField extends OptionsField {
  150. type = 'Radio';
  151. }
  152. class CheckboxField extends SimpleField {
  153. type = 'Checkbox';
  154. isChecked: boolean;
  155. default: any = false;
  156. checkedValue: boolean|number|string = true;
  157. rowLabel: null;
  158. constructor(meta: ISimpleFieldMetaData) {
  159. super(meta);
  160. if (meta.value) {
  161. this.checkedValue = meta.value;
  162. }
  163. if (meta.checkedValue) {
  164. this.checkedValue = meta.checkedValue;
  165. }
  166. if (meta.default) {
  167. this.default = meta.default;
  168. }
  169. }
  170. }
  171. class HiddenField extends SimpleField {
  172. type = 'Hidden';
  173. }
  174. // ---------------------------------------------------------------------------------------------------------------------
  175. // Concrete Implementations - Custom Form Components
  176. class CheckbuttonField extends CheckboxField {
  177. type = 'Checkbutton';
  178. }
  179. class DropdownModifiedInputField extends SimpleField {
  180. type = 'DropdownModifiedInput';
  181. modifiers: string[] = standardModifiers;
  182. transform: ValueTransformer = standardTransformer;
  183. constructor(meta: IDropdownModifiedInputFieldMetaData) {
  184. super(meta);
  185. }
  186. }
  187. class MultilineField extends SimpleField {
  188. type = 'Multiline';
  189. lines: number;
  190. maxLineLength: number;
  191. }
  192. // ---------------------------------------------------------------------------------------------------------------------
  193. // Concrete Implementations - Custom FormGroup Components (which render a group of FormControls)
  194. class CheckboxGroup {
  195. type = 'CheckboxGroup';
  196. name: string;
  197. label?: string;
  198. groupName: string;
  199. firstEnablesRest?: boolean;
  200. showAllOrNone?: boolean;
  201. meta: CheckbuttonField[] | { [key: string]: CheckbuttonField };
  202. constructor(groupmeta: any) {
  203. Object.assign(this, groupmeta);
  204. if (typeof this.label === 'undefined') {
  205. // If label is not supplied set it to the unCamelCased'n'Spaced name
  206. // e.g. supervisorCardNumber --> Supervisor Card Number
  207. this.label = unCamelCase(this.name);
  208. }
  209. // Can render as a FormArray or FormGroup depending on input data
  210. if (Array.isArray(groupmeta.meta)) {
  211. const arrayMembers = groupmeta.meta;
  212. this.meta = arrayMembers.map(cb => new CheckbuttonField(cb));
  213. } else {
  214. const groupMembers = groupmeta.meta;
  215. this.meta = Object.entries(groupMembers)
  216. .map( ([key, cb]) => [key, new CheckbuttonField(cb as ISimpleFieldMetaData)] )
  217. .reduce((res, [key, cbf]) => { res[key as string] = cbf; return res; }, {});
  218. }
  219. }
  220. }
  221. class CheckbuttonGroup extends CheckboxGroup {
  222. type = 'CheckbuttonGroup';
  223. }
  224. // ---------------------------------------------------------------------------------------------------------------------
  225. // Concrete Implementations - Kendo Form Components
  226. class TimepickerField extends SimpleField {
  227. type = 'Timepicker';
  228. value: Date | string;
  229. format = 'hh:mm a';
  230. steps = { hour: 1, minute: 15 };
  231. constructor(meta) {
  232. super(meta);
  233. if (typeof this.value === 'string') {
  234. const [hh, mm, ss] = this.value.split(':');
  235. this.value = new Date(2000, 6, 1, +hh || 0, +mm || 0, +ss || 0);
  236. }
  237. if (!(this.value instanceof Date)) {
  238. this.value = new Date();
  239. }
  240. }
  241. }
  242. class ClrDatepickerField extends SimpleField {
  243. type = 'ClrDatepicker';
  244. value: Date = new Date();
  245. }
  246. // ---------------------------------------------------------------------------------------------------------------------
  247. // Containers
  248. class Container {
  249. type = 'Container';
  250. name: string;
  251. label = '';
  252. seed: StringMap<any>;
  253. template?: TemplateRef<any>;
  254. button: string;
  255. focussed: boolean = true;
  256. meta: StringMap<any>; // TODO: Tighten up on type with union type
  257. constructor(containerMeta: StringMap<any>) {
  258. Object.assign(this, containerMeta);
  259. if (typeof this.label === 'undefined') {
  260. this.label = unCamelCase(this.name);
  261. }
  262. }
  263. }
  264. class RepeatingContainer {
  265. type = 'RepeatingContainer';
  266. name: string;
  267. prefix: string = 'group';
  268. label = '';
  269. seed: StringMap<any>;
  270. template?: TemplateRef<any>;
  271. meta: Container[]; // An array of Containers
  272. minRepeat: number = 1;
  273. maxRepeat: number = 10;
  274. initialRepeat: number;
  275. showAddControl: boolean = true;
  276. showDeleteControl: boolean = true;
  277. primaryField: string = '';
  278. display: string = 'SINGLE'; // Display strategy to use - ALL or SINGLE - All at once, or one at a time (with a switcher)
  279. constructor(containerMeta: StringMap<any>) {
  280. Object.assign(this, containerMeta);
  281. if (typeof this.label === 'undefined') {
  282. this.label = unCamelCase(this.name);
  283. }
  284. if (!containerMeta.initialRepeat) {
  285. this.initialRepeat = containerMeta.meta.length;
  286. }
  287. this.meta = containerMeta.meta.map((m, i) => new Container({
  288. name: `${this.prefix}${i+1}`,
  289. meta: m,
  290. button: unCamelCase(`${this.prefix}${i+1}`),
  291. focussed: this.display === 'SINGLE' ? i === 0 : true
  292. }));
  293. }
  294. }
  295. // ---------------------------------------------------------------------------------------------------------------------
  296. // Button Group
  297. interface IButtonInterface {
  298. label: string;
  299. fnId: string;
  300. class?: string;
  301. icon?: string;
  302. }
  303. class Button implements IButtonInterface {
  304. label;
  305. fnId;
  306. class = 'btn-primary';
  307. constructor(buttonProps) {
  308. Object.assign(this, buttonProps);
  309. }
  310. }
  311. class ButtonGroup {
  312. type = 'ButtonGroup';
  313. name: string;
  314. label = '';
  315. meta: Button[];
  316. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  317. constructor(meta) {
  318. Object.assign(this, meta);
  319. this.meta = this.meta.map(b => b instanceof Button ? b : new Button(b));
  320. }
  321. }
  322. // ---------------------------------------------------------------------------------------------------------------------
  323. // Heading
  324. class Heading {
  325. type = 'Heading';
  326. text = 'Missing Heading Text';
  327. level = 3;
  328. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  329. readonly noLabel = true; // Indicates this has no label, so don't create normal form row
  330. constructor(meta) {
  331. Object.assign(this, meta);
  332. }
  333. }
  334. // ---------------------------------------------------------------------------------------------------------------------
  335. // Display
  336. class DisplayField {
  337. value: string;
  338. link?: ILink;
  339. readonly noFormControls = true; // Indicates this has no FormControls associated with it
  340. constructor(meta) {
  341. Object.assign(this, meta);
  342. }
  343. }
  344. // ---------------------------------------------------------------------------------------------------------------------
  345. // ---------------------------------------------------------------------------------------------------------------------
  346. // ---------------------------------------------------------------------------------------------------------------------
  347. // Exports
  348. export {
  349. SimpleField,
  350. TextField, TextareaField, PasswordField, SelectField, RadioField, CheckboxField, HiddenField,
  351. CheckbuttonField, DropdownModifiedInputField, MultilineField,
  352. CheckboxGroup, CheckbuttonGroup,
  353. TimepickerField, ClrDatepickerField,
  354. Container, RepeatingContainer,
  355. ButtonGroup, Heading, DisplayField
  356. };