Ver código fonte

Merging changes from AMP, particularly for change detection and form validation

Richard Knight 5 anos atrás
pai
commit
230e0aef10
68 arquivos alterados com 748 adições e 473 exclusões
  1. 16 5
      src/app/dynaform/components/_abstract/group-input.component.ts
  2. 32 6
      src/app/dynaform/components/_abstract/native-input.component.ts
  3. 1 0
      src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.html
  4. 11 4
      src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.ts
  5. 8 5
      src/app/dynaform/components/clarity/datepicker/datepicker.component.ts
  6. 17 0
      src/app/dynaform/components/clarity/display/clr-display.component.html
  7. 0 0
      src/app/dynaform/components/clarity/display/clr-display.component.scss
  8. 5 5
      src/app/dynaform/components/native/text/text.component.spec.ts
  9. 27 0
      src/app/dynaform/components/clarity/display/clr-display.component.ts
  10. 3 2
      src/app/dynaform/components/clarity/password/clr-password.component.html
  11. 10 0
      src/app/dynaform/components/clarity/password/clr-password.component.ts
  12. 3 2
      src/app/dynaform/components/clarity/radio/clr-radio.component.html
  13. 13 4
      src/app/dynaform/components/clarity/radio/clr-radio.component.ts
  14. 8 5
      src/app/dynaform/components/clarity/select/clr-select.component.html
  15. 19 4
      src/app/dynaform/components/clarity/select/clr-select.component.ts
  16. 7 5
      src/app/dynaform/components/clarity/text/clr-text.component.html
  17. 11 0
      src/app/dynaform/components/clarity/text/clr-text.component.ts
  18. 1 1
      src/app/dynaform/components/clarity/textarea/clr-textarea.component.html
  19. 4 0
      src/app/dynaform/components/clarity/textarea/clr-textarea.component.ts
  20. 9 3
      src/app/dynaform/components/custom/checkbutton/checkbutton.component.ts
  21. 10 0
      src/app/dynaform/components/custom/datetime/datetime.component.html
  22. 0 0
      src/app/dynaform/components/custom/datetime/datetime.component.scss
  23. 6 6
      src/app/dynaform/components/native/password/password.component.spec.ts
  24. 78 0
      src/app/dynaform/components/custom/datetime/datetime.component.ts
  25. 3 2
      src/app/dynaform/components/custom/dropdown-modified-input/dropdown-modified-input.component.ts
  26. 4 2
      src/app/dynaform/components/custom/multiline/multiline.component.ts
  27. 10 8
      src/app/dynaform/components/group/checkbox-group/clr-checkbox-group.component.ts
  28. 7 5
      src/app/dynaform/components/group/checkbutton-group/checkbutton-group.component.ts
  29. 4 5
      src/app/dynaform/components/index.ts
  30. 0 1
      src/app/dynaform/components/native/hidden/hidden.component.html
  31. 0 25
      src/app/dynaform/components/native/hidden/hidden.component.spec.ts
  32. 0 11
      src/app/dynaform/components/native/hidden/hidden.component.ts
  33. 0 5
      src/app/dynaform/components/native/password/password.component.html
  34. 0 15
      src/app/dynaform/components/native/password/password.component.ts
  35. 0 6
      src/app/dynaform/components/native/radio/radio.component.html
  36. 0 0
      src/app/dynaform/components/native/radio/radio.component.scss
  37. 0 25
      src/app/dynaform/components/native/radio/radio.component.spec.ts
  38. 0 25
      src/app/dynaform/components/native/radio/radio.component.ts
  39. 0 14
      src/app/dynaform/components/native/select/select.component.html
  40. 0 0
      src/app/dynaform/components/native/select/select.component.scss
  41. 0 25
      src/app/dynaform/components/native/select/select.component.spec.ts
  42. 0 28
      src/app/dynaform/components/native/select/select.component.ts
  43. 0 21
      src/app/dynaform/components/native/text/text.component.html
  44. 0 0
      src/app/dynaform/components/native/text/text.component.scss
  45. 0 17
      src/app/dynaform/components/native/text/text.component.ts
  46. 0 6
      src/app/dynaform/components/native/textarea/textarea.component.html
  47. 0 0
      src/app/dynaform/components/native/textarea/textarea.component.scss
  48. 0 25
      src/app/dynaform/components/native/textarea/textarea.component.spec.ts
  49. 0 15
      src/app/dynaform/components/native/textarea/textarea.component.ts
  50. 1 1
      src/app/dynaform/components/nocontrol/button-group/button-group.component.html
  51. 8 2
      src/app/dynaform/components/nocontrol/button-group/button-group.component.ts
  52. 0 11
      src/app/dynaform/components/nocontrol/display/display.component.html
  53. 0 0
      src/app/dynaform/components/nocontrol/display/display.component.scss
  54. 0 25
      src/app/dynaform/components/nocontrol/display/display.component.spec.ts
  55. 0 21
      src/app/dynaform/components/nocontrol/display/display.component.ts
  56. 9 2
      src/app/dynaform/components/nocontrol/heading/heading.component.ts
  57. 20 0
      src/app/dynaform/config/validation-messages.config.ts
  58. 58 16
      src/app/dynaform/directives/dynafield.directive.ts
  59. 17 10
      src/app/dynaform/dynaform.component.ts
  60. 4 2
      src/app/dynaform/dynaform.module.ts
  61. 2 0
      src/app/dynaform/index.ts
  62. 11 0
      src/app/dynaform/interfaces/index.ts
  63. 12 14
      src/app/dynaform/models/field.model.ts
  64. 140 22
      src/app/dynaform/services/_formdata-utils.ts
  65. 54 1
      src/app/dynaform/services/dynaform.service.ts
  66. 53 0
      src/app/dynaform/services/friendly-validation-errors.service.ts
  67. 3 3
      src/app/dynaform/testdata/testset.1.ts
  68. 29 0
      src/app/dynaform/testdata/testset.5.ts

+ 16 - 5
src/app/dynaform/components/_abstract/group-input.component.ts

@@ -7,22 +7,33 @@ export abstract class GroupInputComponent implements OnInit {
 	control: FormGroup | FormArray;
 
 	@Input()
-	meta;
+	set meta(meta: StringMap<any>) {
+		this._meta = meta;
+		this.exposeForTemplate();
+	};
 
 	formGroup: FormGroup;
 	childMetaArray: Array<StringMap<any>>;
 	controlNames: Array<string>;
 
+	readonly componentName: string;
 	exposeMetaInTemplate: string[] = [];
+	_meta: StringMap<any>
 
 	ngOnInit() {
-		// Move meta variables up a level, for direct access in templates
-		this.exposeMetaInTemplate.map(p => this[p] = this.meta[p] !== undefined ? this.meta[p] : this[p]);
-
 		// Get the FormGroup, and information about the controls inside it
 		this.formGroup = this.control as FormGroup;
-		this.childMetaArray = Object.values(this.meta.meta); // Metadata array of all controls in group
+		this.childMetaArray = Object.values(this._meta.meta); // Metadata array of all controls in group
 		this.controlNames = Object.keys(this.formGroup.controls);
 	}
 
+	exposeForTemplate() {
+		// Move meta variables up a level, for direct access in templates
+		this.exposeMetaInTemplate.map(p => this[p] = this._meta[p] !== undefined ? this._meta[p] : this[p]);
+	}
+
+	getName(): string {
+		return this.componentName;
+	}
+
 }

+ 32 - 6
src/app/dynaform/components/_abstract/native-input.component.ts

@@ -1,26 +1,52 @@
-import { Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { Input, Output, EventEmitter } from '@angular/core';
 import { FormControl } from '@angular/forms';
+import { FriendlyValidationErrorsService } from './../../services/friendly-validation-errors.service';
 
-export abstract class NativeInputComponent implements OnInit {
+export abstract class NativeInputComponent {
 
 	@Input()
 	control: FormControl;
 
 	@Input()
-	meta;
+	set meta(meta: StringMap<any>) {
+		this._meta = meta;
+		this.exposeForTemplate();
+	}
 
 	@Output()
 	call: EventEmitter<string> = new EventEmitter<string>();
 
+	readonly componentName: string;
 	exposeMetaInTemplate: string[] = [];
+	_meta: StringMap<any>;
+
+	constructor(protected valErrsService: FriendlyValidationErrorsService) {}
 
-	ngOnInit() {
+	exposeForTemplate() {
 		// Move meta variables up a level, for direct access in templates
-		this.exposeMetaInTemplate.map(p => this[p] = this.meta[p] !== undefined ? this.meta[p] : this[p]);
+		this.exposeMetaInTemplate.map(p => this[p] = this._meta[p] !== undefined ? this._meta[p] : this[p]);
 	}
 
 	handle(fnId: string, val: any): void {
-		this.call.emit(fnId);
+		this.call.emit(fnId); // Add value
+	}
+
+	handleChange(): void {
+		if (this._meta.change) {
+			this.handle(this._meta.change, this.control.value);
+		}
+	}
+
+	getName(): string {
+		return this.componentName;
+	}
+
+	getFirstFailureMsg(): string | false {
+		if (!this.control.errors) {
+			return false;
+		}
+		const key = Object.keys(this.control.errors)[0];
+		return this._meta.valFailureMsgs[key] || this.valErrsService.getFriendly(key, this.control.errors[key]);
 	}
 
 }

+ 1 - 0
src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.html

@@ -1,5 +1,6 @@
 <clr-checkbox-wrapper>
 	<input type="checkbox" clrCheckbox [formControl]="control" (change)="setValue($event.target)">
 	<label>{{ label }}</label>
+	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 </clr-checkbox-wrapper>
 

+ 11 - 4
src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.ts

@@ -1,5 +1,6 @@
 import { Component } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
+import { FriendlyValidationErrorsService } from './../../../services/friendly-validation-errors.service';
 
 
 @Component({
@@ -11,11 +12,17 @@ export class ClrCheckboxComponent extends NativeInputComponent {
 
 	exposeMetaInTemplate: string[] = ['label'];
 
+	label: string;
+
+	readonly componentName = 'CheckboxComponent'; // For AOT compatibility, as class names don't survive minification
+
 	setValue(cb: HTMLInputElement) {
-		this.control.setValue(cb.checked ? this.meta.checkedValue : false);
-		if (this.meta.change) {
-			this.handle(this.meta.change, this.control.value);
-		}
+		this.control.setValue(cb.checked ? this._meta.checkedValue : false);
+		this.handleChange();
+	}
+
+	constructor(protected valErrsService: FriendlyValidationErrorsService) {
+		super(valErrsService);
 	}
 
 }

+ 8 - 5
src/app/dynaform/components/clarity/datepicker/datepicker.component.ts

@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
 
 @Component({
@@ -6,14 +6,17 @@ import { NativeInputComponent } from '../../_abstract/native-input.component';
 	templateUrl: './datepicker.component.html',
 	styleUrls: ['./datepicker.component.scss']
 })
-export class ClrDatepickerComponent extends NativeInputComponent {
+export class ClrDatepickerComponent extends NativeInputComponent implements OnInit {
 
 	exposeMetaInTemplate: string[] = ['extraClass', 'placeholder'];
 
+	extraClass: string;
+	placeholder: string;
+
+	readonly componentName = 'DatepickerComponent'; // For AOT compatibility, as class names don't survive minification
+
 	ngOnInit() {
-		super.ngOnInit();
-		
-		// CLarity datepicker expects a string when used reactively
+		// Clarity datepicker expects a string when used reactively
 		const dateObj = this.control.value;
 		const d = dateObj.getDate();
 		const m = dateObj.getMonth();

+ 17 - 0
src/app/dynaform/components/clarity/display/clr-display.component.html

@@ -0,0 +1,17 @@
+<clr-input-container *ngIf="!link; else fieldWithLink">
+	<label>{{ label }}</label>
+	<input clrInput type="text" [formControl]="control" spellcheck="false" disabled>
+</clr-input-container>
+
+
+<ng-template #fieldWithLink>
+	<clr-input-container>
+		<label>{{ label }}</label>
+		<input clrInput type="text" [formControl]="control" spellcheck="false" disabled>
+		<div class="input-group-append">
+			<button class="btn btn-outline" type="button" [routerLink]="[ link.route, control.value ]">
+				{{ link.label || 'Details' }}
+			</button>
+		</div>
+	</clr-input-container>
+</ng-template>

src/app/dynaform/components/native/hidden/hidden.component.scss → src/app/dynaform/components/clarity/display/clr-display.component.scss


+ 5 - 5
src/app/dynaform/components/native/text/text.component.spec.ts

@@ -1,20 +1,20 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { TextComponent } from './text.component';
+import { ClrDisplayComponent } from './clr-display.component';
 
 describe('TextComponent', () => {
-  let component: TextComponent;
-  let fixture: ComponentFixture<TextComponent>;
+  let component: ClrDisplayComponent;
+  let fixture: ComponentFixture<ClrDisplayComponent>;
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
-      declarations: [ TextComponent ]
+      declarations: [ ClrDisplayComponent ]
     })
     .compileComponents();
   }));
 
   beforeEach(() => {
-    fixture = TestBed.createComponent(TextComponent);
+    fixture = TestBed.createComponent(ClrDisplayComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
   });

+ 27 - 0
src/app/dynaform/components/clarity/display/clr-display.component.ts

@@ -0,0 +1,27 @@
+import { Component, OnChanges } from '@angular/core';
+import { NativeInputComponent } from '../../_abstract/native-input.component';
+
+@Component({
+	selector: 'app-clr-display',
+	templateUrl: './clr-display.component.html',
+	styleUrls: ['./clr-display.component.scss']
+})
+export class ClrDisplayComponent extends NativeInputComponent implements OnChanges {
+
+	exposeMetaInTemplate: string[] = ['label', 'link'];
+
+	label: string;
+	link: string;
+
+	readonly componentName = 'DisplayComponent'; // For AOT compatibility, as class names don't survive minification
+
+	constructor() {
+		super(null);
+	}
+
+	ngOnChanges() {
+		console.log(this._meta);
+		console.log(this.control);
+	}
+
+}

+ 3 - 2
src/app/dynaform/components/clarity/password/clr-password.component.html

@@ -1,4 +1,5 @@
 <clr-password-container>
-	<label>{{ label }}</label>
-    <input clrPassword [formControl]="control" [placeholder]="placeholder" />
+	<label [ngClass]="{ 'label-error': control.touched && control.invalid }">{{ label }}</label>
+	<input clrPassword [formControl]="control" [placeholder]="placeholder" />
+	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 </clr-password-container>

+ 10 - 0
src/app/dynaform/components/clarity/password/clr-password.component.ts

@@ -1,5 +1,6 @@
 import { Component } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
+import { FriendlyValidationErrorsService } from './../../../services/friendly-validation-errors.service';
 
 @Component({
 	selector: 'app-password',
@@ -10,4 +11,13 @@ export class ClrPasswordComponent extends NativeInputComponent {
 
 	exposeMetaInTemplate: string[] = ['label', 'placeholder'];
 
+	label: string;
+	placeholder: string;
+
+	readonly componentName = 'PasswordComponent'; // For AOT compatibility, as class names don't survive minification
+
+	constructor(protected valErrsService: FriendlyValidationErrorsService) {
+		super(valErrsService);
+	}
+
 }

+ 3 - 2
src/app/dynaform/components/clarity/radio/clr-radio.component.html

@@ -1,7 +1,8 @@
-<div class="clr-radio-container" [ngClass]="{'clr-control-inline' : horizontal}">
-	<label class="small">{{ label }}</label>
+<div class="clr-radio-container" [ngClass]="{ 'clr-control-inline' : horizontal, 'ng-touched': control.touched, 'ng-invalid': control.invalid }">
+	<label [ngClass]="{ 'label-error': control.touched && control.invalid }">{{ label }}</label>
 	<clr-radio-wrapper *ngFor="let opt of options; let i = index;">
 		<input type="radio" clrRadio [formControl]="control" [value]="opt.value" [name]="prefix" [id]="prefix + i">
 		<label [for]="prefix + i">{{ opt.label }}</label>
 	</clr-radio-wrapper>
+	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 </div>

+ 13 - 4
src/app/dynaform/components/clarity/radio/clr-radio.component.ts

@@ -1,5 +1,7 @@
 import { Component } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
+import { IOption } from '../../../models/field.model';
+import { FriendlyValidationErrorsService } from './../../../services/friendly-validation-errors.service';
 
 @Component({
 	selector: 'app-radio',
@@ -9,11 +11,18 @@ import { NativeInputComponent } from '../../_abstract/native-input.component';
 export class ClrRadioComponent extends NativeInputComponent {
 
 	exposeMetaInTemplate: string[] = ['name', 'label', 'options', 'horizontal'];
-	prefix: string;
 
-	constructor() {
-		super();
-		this.prefix = 'radio_u_' + Math.floor((Math.random() * 10000)).toString();
+	name: string;
+	label: string;
+	options: IOption[];
+	horizontal; boolean;
+
+	readonly prefix = 'radio_u_' + Math.floor((Math.random() * 10000)).toString();
+
+	readonly componentName = 'RadioComponent'; // For AOT compatibility, as class names don't survive minification
+
+	constructor(protected valErrsService: FriendlyValidationErrorsService) {
+		super(valErrsService);
 	}
 
 }

+ 8 - 5
src/app/dynaform/components/clarity/select/clr-select.component.html

@@ -1,17 +1,20 @@
-<clr-select-container *ngIf="!link; else fieldWithLink">
-	<label>{{ label }}</label>
-	<select clrSelect [formControl]="control">
+<clr-select-container *ngIf="!link; else fieldWithLink" [ngClass]="{ 'touched-and-valid': control.touched && control.valid }">
+	<label [ngClass]="{ 'label-error': control.touched && control.invalid }">{{ label }}</label>
+	<select clrSelect [formControl]="control" (change)="handleChange()">
 		<option *ngFor="let opt of options" [value]="opt.value">{{ opt.label }}</option>
 	</select>
+	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 </clr-select-container>
 
 <ng-template #fieldWithLink>
-	<clr-select-container class="clr-input-group clr-input-group-sm">
-		<select [formControl]="control" clrSelect #field>
+	<clr-select-container class="clr-input-group clr-input-group-sm"  [ngClass]="{ 'touched-and-valid': control.touched && control.valid }">
+		<label [ngClass]="{ 'label-error': control.touched && control.invalid }">{{ label }}</label>
+		<select [formControl]="control" clrSelect #field (change)="handleChange()">
 			<option *ngFor="let opt of options" [value]="opt.value">{{ opt.label }}</option>
 		</select>
 		<div class="clr-input-group-append">
 			<button class="btn btn-outline-primary" type="button" (click)="navigate(field)">{{ link.label || 'Details' }}</button>
 		</div>
+		<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 	</clr-select-container>
 </ng-template>

+ 19 - 4
src/app/dynaform/components/clarity/select/clr-select.component.ts

@@ -1,6 +1,9 @@
-import { Component } from '@angular/core';
+import { Component, Injector } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
 import { Router, ActivatedRoute } from '@angular/router';
+import { IOption, ILink } from '../../../models/field.model';
+import { FriendlyValidationErrorsService } from './../../../services/friendly-validation-errors.service';
+
 
 @Component({
 	selector: 'app-select',
@@ -11,10 +14,22 @@ export class ClrSelectComponent extends NativeInputComponent {
 
 	exposeMetaInTemplate: string[] = ['label', 'options', 'link'];
 
-	constructor(private router: Router, private route: ActivatedRoute) {
-		super();
+	label: string;
+	options: IOption[];
+	link: ILink;
+
+	readonly componentName = 'SelectComponent'; // For AOT compatibility, as class names don't survive minification
+
+	// TODO: Emit event to parant dynaform to do routing - then all form components will be able to use it
+	// It's cleaner
+	constructor(
+		private router: Router,
+		private route: ActivatedRoute,
+		protected valErrsService: FriendlyValidationErrorsService
+	) {
+		super(valErrsService);
 	}
-	
+
 	navigate(field: HTMLSelectElement) {
 		const base = Array.isArray(this.meta.link.route) ? this.meta.link.route : [this.meta.link.route];
 		const destination = [...base, field.options[field.selectedIndex].value];

+ 7 - 5
src/app/dynaform/components/clarity/text/clr-text.component.html

@@ -1,16 +1,18 @@
 <clr-input-container *ngIf="!link; else fieldWithLink">
-	<label>{{ label }}</label>
-	<input clrInput [formControl]="control" [placeholder]="placeholder">
+	<label [ngClass]="{ 'label-error': control.touched && control.invalid }">{{ label }}</label>
+	<input clrInput type="text" [formControl]="control" [placeholder]="placeholder" spellcheck="false">
+	<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 </clr-input-container>
 
 
 <ng-template #fieldWithLink>
 	<clr-input-container>
-		<label>{{ label }}</label>
-		<input clrInput #field [formControl]="control" [placeholder]="placeholder">
+		<label [ngClass]="{ 'label-error': control.touched && control.invalid }">{{ label }}</label>
+		<input clrInput type="text" #field [formControl]="control" [placeholder]="placeholder" spellcheck="false">
 		<div class="input-group-append">
 			<button class="btn btn-outline" type="button"
 				[routerLink]="[ link.route, field.value ]">{{ link.label || 'Details' }}</button>
 		</div>
+		<clr-control-error>{{ getFirstFailureMsg() }}</clr-control-error>
 	</clr-input-container>
-</ng-template>
+</ng-template>

+ 11 - 0
src/app/dynaform/components/clarity/text/clr-text.component.ts

@@ -1,5 +1,6 @@
 import { Component } from '@angular/core';
 import { NativeInputComponent } from '../../_abstract/native-input.component';
+import { FriendlyValidationErrorsService } from './../../../services/friendly-validation-errors.service';
 
 @Component({
 	selector: 'app-clr-text',
@@ -10,4 +11,14 @@ export class ClrTextComponent extends NativeInputComponent {
 
 	exposeMetaInTemplate: string[] = ['label', 'placeholder', 'link'];
 
+	label: string;
+	placeholder: string;
+	link: string;
+
+	readonly componentName = 'TextComponent'; // For AOT compatibility, as class names don't survive minification
+
+	constructor(protected valErrsService: FriendlyValidationErrorsService) {
+		super(valErrsService);
+	}
+
 }

+ 1 - 1
src/app/dynaform/components/clarity/textarea/clr-textarea.component.html

@@ -2,4 +2,4 @@
 	[formControl]="control"
 	[placeholder]="placeholder"
 	rows="5"
-></textarea>
+></textarea>

+ 4 - 0
src/app/dynaform/components/clarity/textarea/clr-textarea.component.ts

@@ -10,4 +10,8 @@ export class ClrTextareaComponent extends NativeInputComponent {
 
 	exposeMetaInTemplate: string[] = ['placeholder'];
 
+	placeholder: string;
+
+	readonly componentName = 'TextareaComponent'; // For AOT compatibility, as class names don't survive minification
+
 }

+ 9 - 3
src/app/dynaform/components/custom/checkbutton/checkbutton.component.ts

@@ -1,6 +1,7 @@
 import { Component, OnChanges, forwardRef, ChangeDetectorRef } from '@angular/core';
 import { NG_VALUE_ACCESSOR } from '@angular/forms';
 import { CustomInputComponent } from './../../_abstract/custom-input.component';
+import { FriendlyValidationErrorsService } from './../../../services/friendly-validation-errors.service';
 
 @Component({
 	selector: 'app-checkbutton',
@@ -26,12 +27,17 @@ export class CheckbuttonComponent extends CustomInputComponent implements OnChan
 	checkedValue: string | number | boolean = true;
 	onChange: (val) => void;
 
-	constructor(private _cdr: ChangeDetectorRef) {
-		super();
+	readonly componentName = 'CheckbuttonComponent'; // For AOT compatibility, as class names don't survive minification
+
+	constructor(
+		protected valErrsService: FriendlyValidationErrorsService,
+		private _cdr: ChangeDetectorRef
+	) {
+		super(valErrsService);
 	}
 
 	ngOnChanges() {
-		this.disabled = this.meta.disabled;
+		this.disabled = this._meta.disabled;
 	}
 
 	toggleChecked(e?: MouseEvent): void {

+ 10 - 0
src/app/dynaform/components/custom/datetime/datetime.component.html

@@ -0,0 +1,10 @@
+<label>{{ label }}</label>
+<clr-date-container>
+	<input clrDate [ngModel]="date" (ngModelChange)="updateValue('date', $event)">
+</clr-date-container>
+<select clrSelect [ngModel]="hh" (ngModelChange)="updateValue('hours', $event)">
+	<option *ngFor="let i of hourOptions" [value]="i">{{ i }}</option>
+</select>
+<select clrSelect [ngModel]="mm" (ngModelChange)="updateValue('minutes', $event)">
+	<option *ngFor="let i of minuteOptions" [value]="i">{{ i }}</option>
+</select>

src/app/dynaform/components/native/password/password.component.scss → src/app/dynaform/components/custom/datetime/datetime.component.scss


+ 6 - 6
src/app/dynaform/components/native/password/password.component.spec.ts

@@ -1,20 +1,20 @@
 import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { PasswordComponent } from './password.component';
+import { DatetimeComponent } from './datetime.component';
 
-describe('PasswordComponent', () => {
-  let component: PasswordComponent;
-  let fixture: ComponentFixture<PasswordComponent>;
+describe('DatetimeComponent', () => {
+  let component: DatetimeComponent;
+  let fixture: ComponentFixture<DatetimeComponent>;
 
   beforeEach(async(() => {
     TestBed.configureTestingModule({
-      declarations: [ PasswordComponent ]
+      declarations: [ DatetimeComponent ]
     })
     .compileComponents();
   }));
 
   beforeEach(() => {
-    fixture = TestBed.createComponent(PasswordComponent);
+    fixture = TestBed.createComponent(DatetimeComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
   });

+ 78 - 0
src/app/dynaform/components/custom/datetime/datetime.component.ts

@@ -0,0 +1,78 @@
+import { Component, OnInit, forwardRef } from '@angular/core';
+import { NG_VALUE_ACCESSOR } from '@angular/forms';
+import { CustomInputComponent } from './../../_abstract/custom-input.component';
+
+@Component({
+	selector: 'amp-datetime',
+	templateUrl: './datetime.component.html',
+	styleUrls: ['./datetime.component.scss'],
+	providers: [
+		{
+			provide: NG_VALUE_ACCESSOR,
+			useExisting: forwardRef(() => DatetimeComponent),
+			multi: true
+		}
+	]
+})
+export class DatetimeComponent extends CustomInputComponent implements OnInit {
+
+	exposeMetaInTemplate: string[] = ['label'];
+
+	label: string;
+
+	value: Date;
+
+	date: string;
+	mm: string;
+	hh: string;
+	hourOptions: string[] = new Array(24).map((m, i) => `0${i}`.slice(-2));
+	minuteOptions: string[] = new Array(60).map((m, i) => `0${i}`.slice(-2));
+
+	readonly componentName = 'DatetimeComponent'; // For AOT compatibility, as class names don't survive minification
+
+	ngOnInit() {
+		console.log(this.control.value);
+		this.splitIntoDateAndTime(this.control.value);
+	}
+
+	writeValue(value: any): void {
+		this.value = value;
+		this.splitIntoDateAndTime(value);
+	}
+
+	trackByFn(index: any, item: any) {
+		return index;
+	}
+
+	updateValue(id: 'date'|'hours'|'minutes', value: string): void {
+		switch (id) {
+			case 'date':
+				this.date = value;
+				break;
+			case 'hours':
+				this.hh = value;
+				break;
+			case 'minutes':
+				this.mm = value;
+				break;
+		}
+		this.value = this.recombineDateAndTime();
+		this.propagateChange(this.value);
+	}
+
+	private splitIntoDateAndTime(value: string): void {
+		const dateObj = new Date(value);
+		const d = dateObj.getDate();
+		const m = dateObj.getMonth();
+		const y = dateObj.getFullYear();
+		this.date = `${d}/${m + 1}/${y}`;
+		this.hh = `0${dateObj.getHours().toString()}`.slice(-2);
+		this.mm = `0${dateObj.getMinutes().toString()}`.slice(-2);
+	}
+
+	private recombineDateAndTime() {
+		const [ d, m, y ] = this.date.split('/');
+		return new Date(parseInt(y), parseInt(m) - 1, parseInt(d), parseInt(this.hh), parseInt(this.mm));
+	}
+
+}

+ 3 - 2
src/app/dynaform/components/custom/dropdown-modified-input/dropdown-modified-input.component.ts

@@ -27,11 +27,12 @@ export class DropdownModifiedInputComponent extends CustomInputComponent impleme
 	extraClass;
 	selectedModifier: string;
 	displayedValue: string;
-	
+
 	private _controlValue: string;
 
+	readonly componentName = 'DropdownModifiedInputComponent'; // For AOT compatibility, as class names don't survive minification
+
 	ngOnInit() {
-		super.ngOnInit();
 		this.controlValue = this.control.value;
 	}
 

+ 4 - 2
src/app/dynaform/components/custom/multiline/multiline.component.ts

@@ -17,14 +17,16 @@ import { CustomInputComponent } from './../../_abstract/custom-input.component';
 export class MultilineComponent extends CustomInputComponent {
 
 	exposeMetaInTemplate: string[] = ['maxLineLength'];
-	
+
 	linesArr: string[];
 	value: string;
 	maxLineLength: number;
 
+	readonly componentName = 'MultilineComponent'; // For AOT compatibility, as class names don't survive minification
+
 	writeValue(value: any): void {
 		this.value = value;
-		this.splitIntoLines(value, this.meta.lines || 5);
+		this.splitIntoLines(value, this._meta.lines || 5);
 	}
 
 	trackByFn(index: any, item: any) {

+ 10 - 8
src/app/dynaform/components/group/checkbox-group/clr-checkbox-group.component.ts

@@ -11,9 +11,11 @@ export class ClrCheckboxGroupComponent extends GroupInputComponent implements On
 
 	firstControl: FormControl;
 
+	readonly componentName = 'ClrCheckboxGroupComponent'; // For AOT compatibility, as class names don't survive minification
+
 	constructor(
 		@Attribute('firstEnablesRest') private firstEnablesRest,
-		@Attribute('allOrNone') private showAllOrNone
+		@Attribute('allOrNone') public showAllOrNone
 	) {
 		super();
 		this.firstEnablesRest = firstEnablesRest === ''; // True if 'firstEnablesRest' exists as component attribute
@@ -22,11 +24,11 @@ export class ClrCheckboxGroupComponent extends GroupInputComponent implements On
 
 	ngOnInit() {
 		super.ngOnInit();
-		if (this.meta.firstEnablesRest) {
-			this.firstEnablesRest = this.meta.firstEnablesRest;
+		if (this._meta.firstEnablesRest) {
+			this.firstEnablesRest = this._meta.firstEnablesRest;
 		}
-		if (this.meta.showAllOrNone) {
-			this.showAllOrNone = this.meta.showAllOrNone;
+		if (this._meta.showAllOrNone) {
+			this.showAllOrNone = this._meta.showAllOrNone;
 		}
 		if (this.firstEnablesRest) {
 			this.firstControl = this.formGroup.controls[this.controlNames[0]] as FormControl;
@@ -47,13 +49,13 @@ export class ClrCheckboxGroupComponent extends GroupInputComponent implements On
 		}
 	}
 
-	selectAll(e: Event): false {
-		this.controlNames.forEach(c => this.formGroup.get(c).setValue(this.meta.meta[c].value));
+	selectAll(e: MouseEvent): false {
+		this.controlNames.forEach(c => this.formGroup.get(c).setValue(this._meta.meta[c].value));
 		(e.target as HTMLLinkElement).blur();
 		return false;
 	}
 
-	selectNone(e: Event): false {
+	selectNone(e: MouseEvent): false {
 		this.controlNames.forEach(c => this.formGroup.get(c).setValue(null));
 		(e.target as HTMLLinkElement).blur();
 		return false;

+ 7 - 5
src/app/dynaform/components/group/checkbutton-group/checkbutton-group.component.ts

@@ -11,6 +11,8 @@ export class CheckbuttonGroupComponent extends GroupInputComponent implements On
 
 	firstControl: FormControl;
 
+	readonly componentName = 'CheckbuttonGroupComponent'; // For AOT compatibility, as class names don't survive minification
+
 	constructor(
 		@Attribute('firstEnablesRest') private firstEnablesRest,
 		@Attribute('allOrNone') public showAllOrNone
@@ -22,11 +24,11 @@ export class CheckbuttonGroupComponent extends GroupInputComponent implements On
 
 	ngOnInit() {
 		super.ngOnInit();
-		if (this.meta.firstEnablesRest) {
-			this.firstEnablesRest = this.meta.firstEnablesRest;
+		if (this._meta.firstEnablesRest) {
+			this.firstEnablesRest = this._meta.firstEnablesRest;
 		}
-		if (this.meta.showAllOrNone) {
-			this.showAllOrNone = this.meta.showAllOrNone;
+		if (this._meta.showAllOrNone) {
+			this.showAllOrNone = this._meta.showAllOrNone;
 		}
 		if (this.firstEnablesRest) {
 			this.firstControl = this.formGroup.controls[this.controlNames[0]] as FormControl;
@@ -48,7 +50,7 @@ export class CheckbuttonGroupComponent extends GroupInputComponent implements On
 	}
 
 	selectAll(e: MouseEvent): false {
-		this.controlNames.forEach(c => this.formGroup.get(c).setValue(this.meta.meta[c].value));
+		this.controlNames.forEach(c => this.formGroup.get(c).setValue(this._meta.meta[c].value));
 		(e.target as HTMLLinkElement).blur();
 		return false;
 	}

+ 4 - 5
src/app/dynaform/components/index.ts

@@ -1,8 +1,6 @@
 // Barrel grouping all form field components
 // See https://basarat.gitbooks.io/typescript/docs/tips/barrel.html
 
-import { HiddenComponent } from './native/hidden/hidden.component';
-
 import { ClrTextComponent as TextComponent } from './clarity/text/clr-text.component';
 import { ClrTextareaComponent as TextareaComponent } from './clarity/textarea/clr-textarea.component';
 import { ClrPasswordComponent as PasswordComponent } from './clarity/password/clr-password.component';
@@ -10,8 +8,10 @@ import { ClrSelectComponent as SelectComponent } from './clarity/select/clr-sele
 import { ClrRadioComponent as RadioComponent } from './clarity/radio/clr-radio.component';
 import { ClrCheckboxComponent as CheckboxComponent } from './clarity/checkbox/clr-checkbox.component';
 import { ClrDatepickerComponent  as DatepickerComponent } from './clarity/datepicker/datepicker.component';
+import { ClrDisplayComponent  as DisplayComponent } from './clarity/display/clr-display.component';
 
 import { CheckbuttonComponent } from './custom/checkbutton/checkbutton.component';
+import { DatetimeComponent } from './custom/datetime/datetime.component';
 import { DropdownModifiedInputComponent } from './custom/dropdown-modified-input/dropdown-modified-input.component';
 import { MultilineComponent } from './custom/multiline/multiline.component';
 import { ClrCheckboxGroupComponent as CheckboxGroupComponent } from './group/checkbox-group/clr-checkbox-group.component';
@@ -19,10 +19,8 @@ import { CheckbuttonGroupComponent } from './group/checkbutton-group/checkbutton
 
 import { ButtonGroupComponent } from './nocontrol/button-group/button-group.component';
 import { HeadingComponent } from './nocontrol/heading/heading.component';
-import { DisplayComponent } from './nocontrol/display/display.component';
 
 export const ffcArr = [
-	HiddenComponent,
 	TextComponent,
 	TextareaComponent,
 	PasswordComponent,
@@ -30,12 +28,13 @@ export const ffcArr = [
 	RadioComponent,
 	CheckboxComponent,
 	DatepickerComponent,
+	DisplayComponent,
 	CheckbuttonComponent,
+	DatetimeComponent,
 	DropdownModifiedInputComponent,
 	MultilineComponent,
 	CheckboxGroupComponent,
 	CheckbuttonGroupComponent,
 	ButtonGroupComponent,
 	HeadingComponent,
-	DisplayComponent
 ];

+ 0 - 1
src/app/dynaform/components/native/hidden/hidden.component.html

@@ -1 +0,0 @@
-<input type="hidden" [formControl]="control">

+ 0 - 25
src/app/dynaform/components/native/hidden/hidden.component.spec.ts

@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { HiddenComponent } from './hidden.component';
-
-describe('HiddenComponent', () => {
-  let component: HiddenComponent;
-  let fixture: ComponentFixture<HiddenComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ HiddenComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(HiddenComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});

+ 0 - 11
src/app/dynaform/components/native/hidden/hidden.component.ts

@@ -1,11 +0,0 @@
-import { Component } from '@angular/core';
-import { NativeInputComponent } from '../../_abstract/native-input.component';
-
-@Component({
-	selector: 'app-hidden',
-	templateUrl: './hidden.component.html',
-	styleUrls: ['./hidden.component.scss']
-})
-export class HiddenComponent extends NativeInputComponent {
-
-}

+ 0 - 5
src/app/dynaform/components/native/password/password.component.html

@@ -1,5 +0,0 @@
-<input type="password"
-	[formControl]="control"
-	[placeholder]="placeholder"
-	class="clr-input"
->

+ 0 - 15
src/app/dynaform/components/native/password/password.component.ts

@@ -1,15 +0,0 @@
-import { Component } from '@angular/core';
-import { NativeInputComponent } from '../../_abstract/native-input.component';
-
-@Component({
-	selector: 'app-password',
-	templateUrl: './password.component.html',
-	styleUrls: ['./password.component.scss']
-})
-export class PasswordComponent extends NativeInputComponent {
-
-	exposeMetaInTemplate: string[] = ['placeholder'];
-
-	placeholder: string;
-
-}

+ 0 - 6
src/app/dynaform/components/native/radio/radio.component.html

@@ -1,6 +0,0 @@
-<div [ngClass]="{'clr-control-inline' : horizontal}">
-	<div *ngFor="let opt of options; let i = index;" class="clr-radio-wrapper">
-		<input type="radio" [formControl]="control" [value]="opt.value" [name]="prefix" [id]="prefix + i" class="clr-radio">
-		<label class="clr-control-label" [for]="prefix + i">{{ opt.label }}</label>
-	</div>
-</div>

+ 0 - 0
src/app/dynaform/components/native/radio/radio.component.scss


+ 0 - 25
src/app/dynaform/components/native/radio/radio.component.spec.ts

@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { RadioComponent } from './radio.component';
-
-describe('RadioComponent', () => {
-  let component: RadioComponent;
-  let fixture: ComponentFixture<RadioComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ RadioComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(RadioComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});

+ 0 - 25
src/app/dynaform/components/native/radio/radio.component.ts

@@ -1,25 +0,0 @@
-import { Component } from '@angular/core';
-import { NativeInputComponent } from '../../_abstract/native-input.component';
-import { IOption } from '../../../models/field.model';
-
-@Component({
-	selector: 'app-radio',
-	templateUrl: './radio.component.html',
-	styleUrls: ['./radio.component.scss']
-})
-export class RadioComponent extends NativeInputComponent {
-
-	exposeMetaInTemplate: string[] = ['name', 'options', 'horizontal'];
-
-	name: string;
-	options: IOption[];
-	horizontal: boolean;
-	
-	prefix: string;
-
-	constructor() {
-		super();
-		this.prefix = 'radio_u_' + Math.floor((Math.random() * 10000)).toString();
-	}
-
-}

+ 0 - 14
src/app/dynaform/components/native/select/select.component.html

@@ -1,14 +0,0 @@
-<select *ngIf="!link; else fieldWithLink" [formControl]="control" class="clr-select">
-	<option *ngFor="let opt of options" [value]="opt.value">{{ opt.label }}</option>
-</select>
-
-<ng-template #fieldWithLink>
-	<div class="input-group input-group-sm">
-		<select [formControl]="control" #field class="clr-select">
-			<option *ngFor="let opt of options" [value]="opt.value">{{ opt.label }}</option>
-		</select>
-		<div class="input-group-append">
-			<button class="btn btn-outline-primary" type="button" (click)="navigate(field)">{{ link.label || 'Details' }}</button>
-		</div>
-	</div>
-</ng-template>

+ 0 - 0
src/app/dynaform/components/native/select/select.component.scss


+ 0 - 25
src/app/dynaform/components/native/select/select.component.spec.ts

@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { SelectComponent } from './select.component';
-
-describe('SelectComponent', () => {
-  let component: SelectComponent;
-  let fixture: ComponentFixture<SelectComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ SelectComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(SelectComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});

+ 0 - 28
src/app/dynaform/components/native/select/select.component.ts

@@ -1,28 +0,0 @@
-import { Component } from '@angular/core';
-import { NativeInputComponent } from '../../_abstract/native-input.component';
-import { Router, ActivatedRoute } from '@angular/router';
-import { IOption, ILink } from '../../../models/field.model';
-
-@Component({
-	selector: 'app-select',
-	templateUrl: './select.component.html',
-	styleUrls: ['./select.component.scss']
-})
-export class SelectComponent extends NativeInputComponent {
-
-	exposeMetaInTemplate: string[] = ['options', 'link'];
-
-	options: IOption[];
-	link: ILink;
-	
-	constructor(private router: Router, private route: ActivatedRoute) {
-		super();
-	}
-	
-	navigate(field: HTMLSelectElement) {
-		const base = Array.isArray(this.meta.link.route) ? this.meta.link.route : [this.meta.link.route];
-		const destination = [...base, field.options[field.selectedIndex].value];
-		this.router.navigate(destination, { relativeTo: this.route });
-	}
-
-}

+ 0 - 21
src/app/dynaform/components/native/text/text.component.html

@@ -1,21 +0,0 @@
-<input *ngIf="!link; else fieldWithLink"
-	type="text"
-	[formControl]="control"
-	[placeholder]="placeholder"
-	class="clr-input"
->
-
-<ng-template #fieldWithLink>
-	<div class="clr-input-group clr-input-group-sm">
-		<input type="text"
-			#field
-			[formControl]="control"
-			[placeholder]="placeholder"
-			class="clr-input"
-		>
-		<div class="clr-input-group-append">
-			<button class="btn btn-outline" type="button"
-				[routerLink]="[ link.route, field.value ]">{{ link.label || 'Details' }}</button>
-		</div>
-	</div>
-</ng-template>

+ 0 - 0
src/app/dynaform/components/native/text/text.component.scss


+ 0 - 17
src/app/dynaform/components/native/text/text.component.ts

@@ -1,17 +0,0 @@
-import { Component } from '@angular/core';
-import { NativeInputComponent } from '../../_abstract/native-input.component';
-import { ILink } from '../../../models/field.model';
-
-@Component({
-	selector: 'app-text',
-	templateUrl: './text.component.html',
-	styleUrls: ['./text.component.scss']
-})
-export class TextComponent extends NativeInputComponent {
-
-	exposeMetaInTemplate: string[] = ['placeholder', 'link'];
-
-	placeholder: string;
-	link: ILink;
-	
-}

+ 0 - 6
src/app/dynaform/components/native/textarea/textarea.component.html

@@ -1,6 +0,0 @@
-<textarea
-	[formControl]="control"
-	[placeholder]="placeholder"
-	class="clr-textarea"
-	rows="5"
-></textarea>

+ 0 - 0
src/app/dynaform/components/native/textarea/textarea.component.scss


+ 0 - 25
src/app/dynaform/components/native/textarea/textarea.component.spec.ts

@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { TextareaComponent } from './textarea.component';
-
-describe('TextareaComponent', () => {
-  let component: TextareaComponent;
-  let fixture: ComponentFixture<TextareaComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ TextareaComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(TextareaComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});

+ 0 - 15
src/app/dynaform/components/native/textarea/textarea.component.ts

@@ -1,15 +0,0 @@
-import { Component } from '@angular/core';
-import { NativeInputComponent } from '../../_abstract/native-input.component';
-
-@Component({
-	selector: 'app-textarea',
-	templateUrl: './textarea.component.html',
-	styleUrls: ['./textarea.component.scss']
-})
-export class TextareaComponent extends NativeInputComponent {
-
-	exposeMetaInTemplate: string[] = ['placeholder'];
-
-	placeholder: string;
-
-}

+ 1 - 1
src/app/dynaform/components/nocontrol/button-group/button-group.component.html

@@ -1,3 +1,3 @@
 <a *ngFor="let button of buttons" class="btn btn-sm" [ngClass]="button.class" (click)="handle(button.fnId, $event)" href>
-	<span *ngIf="button.icon" class="k-icon" [ngClass]="button.icon"></span> {{ button.label }}
+	<clr-icon *ngIf="button.icon" [attr.shape]="button.icon"></clr-icon> {{ button.label }}
 </a>

+ 8 - 2
src/app/dynaform/components/nocontrol/button-group/button-group.component.ts

@@ -1,11 +1,11 @@
-import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { Component, Input, Output, EventEmitter, OnInit, OnChanges } from '@angular/core';
 
 @Component({
 	selector: 'app-button-group',
 	templateUrl: './button-group.component.html',
 	styleUrls: ['./button-group.component.scss']
 })
-export class ButtonGroupComponent implements OnInit {
+export class ButtonGroupComponent implements OnInit, OnChanges {
 
 	@Input()
 	meta: StringMap<any>;
@@ -15,10 +15,16 @@ export class ButtonGroupComponent implements OnInit {
 
 	buttons: StringMap<any>[];
 
+	readonly componentName = 'ButtonGroupComponent'; // For AOT compatibility, as class names don't survive minification
+
 	ngOnInit() {
 		this.buttons = this.meta.meta;
 	}
 
+	ngOnChanges() {
+		this.buttons = this.meta.meta;
+	}
+
 	handle(fnId: string, e: Event): void {
 		e.preventDefault();
 		(e.target as HTMLElement).blur();

+ 0 - 11
src/app/dynaform/components/nocontrol/display/display.component.html

@@ -1,11 +0,0 @@
-<span *ngIf="!link; else displayWithLink">{{ value }}</span>
-
-<ng-template #displayWithLink>
-	<div class="input-group input-group-sm">
-		<span>{{ value }}</span>
-		<div class="input-group-append">
-			<button class="btn btn-outline-primary" type="button"
-				[routerLink]="[ link.route, value ]">{{ link.label || 'Details' }}</button>
-		</div>
-	</div>
-</ng-template>

+ 0 - 0
src/app/dynaform/components/nocontrol/display/display.component.scss


+ 0 - 25
src/app/dynaform/components/nocontrol/display/display.component.spec.ts

@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { DisplayComponent } from './display.component';
-
-describe('DisplayComponent', () => {
-  let component: DisplayComponent;
-  let fixture: ComponentFixture<DisplayComponent>;
-
-  beforeEach(async(() => {
-    TestBed.configureTestingModule({
-      declarations: [ DisplayComponent ]
-    })
-    .compileComponents();
-  }));
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(DisplayComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});

+ 0 - 21
src/app/dynaform/components/nocontrol/display/display.component.ts

@@ -1,21 +0,0 @@
-import { Component, Input, OnInit } from '@angular/core';
-import { ILink } from '../../../models/field.model';
-
-@Component({
-	selector: 'app-display',
-	templateUrl: './display.component.html',
-	styleUrls: ['./display.component.scss']
-})
-export class DisplayComponent implements OnInit {
-
-	@Input()
-	meta: StringMap<any>;
-
-	value: string;
-	link?: ILink;
-
-	ngOnInit() {
-		this.value = this.meta.value;
-		this.link = this.meta.link;
-	}
-}

+ 9 - 2
src/app/dynaform/components/nocontrol/heading/heading.component.ts

@@ -1,11 +1,11 @@
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, Input, OnInit, OnChanges } from '@angular/core';
 
 @Component({
 	selector: 'app-heading',
 	templateUrl: './heading.component.html',
 	styleUrls: ['./heading.component.scss']
 })
-export class HeadingComponent implements OnInit {
+export class HeadingComponent implements OnInit, OnChanges {
 
 	@Input()
 	meta: StringMap<any>;
@@ -13,9 +13,16 @@ export class HeadingComponent implements OnInit {
 	text: string;
 	level: number;
 
+	readonly componentName = 'HeadingComponent'; // For AOT compatibility, as class names don't survive minification
+
 	ngOnInit() {
 		this.text = this.meta.text;
 		this.level = this.meta.level || 2;
 	}
+	
+	ngOnChanges() {
+		this.text = this.meta.text;
+		this.level = this.meta.level || 2;
+	}
 
 }

+ 20 - 0
src/app/dynaform/config/validation-messages.config.ts

@@ -0,0 +1,20 @@
+import { InjectionToken } from '@angular/core';
+import { FriendlyValErrors } from './../interfaces';
+
+// Configure dynaform-wide friendly validation error messages
+// These can be overriden in the metadata for individual controls
+
+export const FRIENDLY_VALIDATION_ERRORS = new InjectionToken<FriendlyValErrors>('validation-messages.config');
+
+export const friendlyValidationErrors: FriendlyValErrors = {
+	'required': 'Required',
+	'requiredtrue': 'Please tick to confirm',
+	'min': 'Pick a number above {{ min }}',
+	'max': 'Pick a number below {{ max }}',
+	'minlength': 'Enter a longer value, at least {{ requiredLength }} characters long',
+	'maxlength': 'Enter a shorter value, no more than {{ requiredLength }} characters long',
+	'email': 'Enter a valid email address',
+	'pattern': 'Invalid'
+};
+
+

+ 58 - 16
src/app/dynaform/directives/dynafield.directive.ts

@@ -1,6 +1,6 @@
 import {
 	Directive, ComponentFactoryResolver, ComponentRef, ViewContainerRef,
-	Input, Output, EventEmitter, OnInit, OnDestroy,
+	Input, Output, EventEmitter, OnInit, OnChanges, OnDestroy,
 	Optional, Self, SkipSelf, Inject
 } from '@angular/core';
 import {
@@ -10,8 +10,26 @@ import {
 	NG_ASYNC_VALIDATORS, AsyncValidatorFn
 } from '@angular/forms';
 
+
 import { ffcArr } from './../components';
-const formFieldComponents = ffcArr.reduce((acc, componentClass) => ({ ...acc, [componentClass.name.replace('Clr', '')]: componentClass }), {});
+// const formFieldComponents = ffcArr.reduce((acc, componentClass) => ({ ...acc, [componentClass.name]: componentClass }), {}); // Works with JIT, but not AOT
+const getFormFieldComponents = () => {
+	return ffcArr.reduce((acc, componentClass) => {
+		try {
+			const componentName = new (componentClass as any)({}, {}).componentName; // Work around AOT name-mangling by explicitly storing the name as a class property
+			if (!componentName) {
+				throw new Error(`All components must have an explicit component name - set componentName property in component's class`);
+			}
+			return { ...acc, [componentName]: componentClass }
+		} catch(e) {
+			console.error(`DYNAFORM: ERROR COMPILING HASH OF FORM FIELD COMPONENTS\n${e}`);
+			console.error(componentClass);
+			return acc;
+		}
+	}, {});
+}
+const formFieldComponents = getFormFieldComponents();
+
 
 interface IFFC {
 	control: FormControl; // Remember, this can be an individual FormControl or a FormGroup
@@ -28,7 +46,7 @@ const componentType = (type: string): string => type[0].toUpperCase() + type.sli
 	// tslint:disable-next-line:directive-selector
 	selector: '[dynafield]'
 })
-export class DynafieldDirective extends NgControl implements OnInit, OnDestroy {
+export class DynafieldDirective extends NgControl implements OnInit, OnChanges, OnDestroy {
 
 	@Input()
 	meta: StringMap<any>;
@@ -76,11 +94,9 @@ export class DynafieldDirective extends NgControl implements OnInit, OnDestroy {
 			const { name, class: cssClass, id: cssId, disabled } = meta;
 
 			// Create the component
-			const component = this.resolver.resolveComponentFactory<IFFC>(formFieldComponents[type]);
-			this.component = this.container.createComponent(component);
+			const componentFactory = this.resolver.resolveComponentFactory<IFFC>(formFieldComponents[type]);
+			this.component = this.container.createComponent(componentFactory);
 			const instance = this.component.instance;
-			const el = this.component.location.nativeElement;
-			el.classList.add(type.toLowerCase().replace('component', ''));
 
 			// Support the recursive insertion of Dynaform components
 			if (type === 'DynaformComponent') {
@@ -90,6 +106,10 @@ export class DynafieldDirective extends NgControl implements OnInit, OnDestroy {
 				meta = meta.meta;
 			}
 
+			// Set id and classes
+			this.setCssId(cssId);
+			this.setCssClasses(type, cssClass);
+
 			// Check whether it's disabled, then set its FormControl and metadata
 			if (disabled) {
 				this.control.reset({ value: this.control.value, disabled: true });
@@ -102,15 +122,6 @@ export class DynafieldDirective extends NgControl implements OnInit, OnDestroy {
 				instance.call.subscribe((fnId: string) => this.call.emit(fnId));
 			}
 
-			// Add id and classes (as specified)
-			if (cssId) {
-				el.id = cssId;
-			}
-			if (cssClass) {
-				const classesToAdd = Array.isArray(cssClass) ? cssClass : [...cssClass.split(/\s+/)];
-				el.classList.add(...classesToAdd);
-			}
-
 			// Connect custom components
 			if (instance.propagateChange) {
 				// We're dealing with a custom form control which implements the ControlValueAccessor interface,
@@ -133,6 +144,18 @@ export class DynafieldDirective extends NgControl implements OnInit, OnDestroy {
 		}
 	}
 
+	ngOnChanges() {
+		// We won't support mutating components (e.g. Text --> Select) at this stage,
+		// but will support mutating an instantiated components metadata
+		// As the component is created in ngOnInt this does nothing in the run before ngOnInit, but responds to later input changes
+		if (this.component) {
+			const { type, class: cssClass, id: cssId } = this.meta;
+			this.setCssId(cssId);
+			this.setCssClasses(type, cssClass);
+			this.component.instance.meta = this.meta;
+		}
+	}
+
 	ngOnDestroy(): void {
 		if (this.formGroupDirective) {
 			this.formGroupDirective.removeControl(this);
@@ -174,5 +197,24 @@ export class DynafieldDirective extends NgControl implements OnInit, OnDestroy {
 		}
 	}
 
+	setCssId(cssId): void {
+		const el = this.component.location.nativeElement;
+		cssId ? el.setAttribute('id', cssId) : el.removeAttribute('id');
+	}
+
+	setCssClasses(type, cssClass): void {
+		const classList = this.component.location.nativeElement.classList as DOMTokenList;
+		const componentTypeClass = type.toLowerCase().replace('component', '');
+		classList.add(componentTypeClass);
+		if (cssClass) {
+			const classesToAdd = Array.isArray(cssClass) ? cssClass : [...cssClass.split(/\s+/)];
+			const classesToRemove = [];
+			const dontRemove = [ ...classesToAdd, componentTypeClass, 'ng-star-inserted' ];
+			classList.forEach(c => classesToRemove.push(dontRemove.indexOf(c) < 0 ? c : null));
+			classList.add(...classesToAdd);
+			classList.remove(...classesToRemove);
+		}
+	}
+
 }
 

+ 17 - 10
src/app/dynaform/dynaform.component.ts

@@ -1,4 +1,4 @@
-import { Component, Input, Output, EventEmitter, TemplateRef, Optional, OnInit, ChangeDetectionStrategy } from '@angular/core';
+import { Component, Input, Output, EventEmitter, TemplateRef, Optional, OnInit, OnChanges, ChangeDetectionStrategy } from '@angular/core';
 import { FormBuilder, FormControl, FormGroup, FormArray, FormGroupName, AbstractControl, ControlContainer } from '@angular/forms';
 import { SuperForm } from 'angular-super-validator';
 import { buildFormGroupFunctionFactory } from './services/_formdata-utils';
@@ -13,9 +13,9 @@ export interface DynarowContext {
 	selector: 'app-dynaform',
 	templateUrl: './dynaform.component.html',
 	styleUrls: ['./dynaform.component.scss'],
-	changeDetection: ChangeDetectionStrategy.Default // or ChangeDetectionStrategy.OnPush - might be more efficient. Experiment later.
+	changeDetection: ChangeDetectionStrategy.Default // or ChangeDetectionStrategy.OnPush - should be more efficient. Experiment later.
 })
-export class DynaformComponent implements OnInit {
+export class DynaformComponent implements OnInit, OnChanges {
 
 	/*
 	 * DynaformComponent: <app-dynaform>
@@ -43,7 +43,9 @@ export class DynaformComponent implements OnInit {
 
 	@Input()
 	set meta(data) {
-		this.formMetaData = this.formMetaData || data;
+		// console.log('Dynaform Set Meta');
+		// this.formMetaData = this.formMetaData || data; // WHY? WHY? WHY? - leave in for now, just in case
+		this.formMetaData = data;
 	}
 
 	@Input()
@@ -65,10 +67,16 @@ export class DynaformComponent implements OnInit {
 	conGreen = 'color: white; background-color: green; font-weight: bold;';
 
 	constructor(
-		@Optional() private cc: ControlContainer,
+		@Optional() private cc: ControlContainer
 	) {}
 
 	ngOnInit() {
+		// console.log('Dyanaform ngOnInit');
+	}
+
+	ngOnChanges() {
+		// Triggered when inputs change
+		// console.log('Dynaform ngOnChanges');
 		// Get the formGroup from the formGroupName if necessary
 		if (!this.formGroup && this.formGroupName) {
 			this.formGroup = this.cc.control as FormGroup; // Get theFormGroup from the injected ControlContainer
@@ -84,7 +92,6 @@ export class DynaformComponent implements OnInit {
 		if (this.debug && this.path.length < 2) {
 			this.displayDebuggingInConsole();
 		}
-
 		// If we're given a formGroupName or nested FormGroup, and the form's full (or partial but fuller) metadata tree,
 		// drill down to find *this* FormGroup's metadata
 		const path = [...this.path]; // Clone to avoid mutating this.path
@@ -128,7 +135,7 @@ export class DynaformComponent implements OnInit {
 	isField(meta: StringMap<any>): boolean {
 		return !meta.type.includes('Container');
 	}
-	
+
 	isRepeatingContainer(meta: StringMap<any>): boolean {
 		return meta.type === 'RepeatingContainer';
 	}
@@ -178,7 +185,7 @@ export class DynaformComponent implements OnInit {
 		const rcMeta = this.formMetaData[repeatingContainerName];
 		const rcFormArray = this.formGroup.get(repeatingContainerName) as FormArray;
 		rcMeta.meta = rcMeta.meta.map( (container, i) => ({ ...container, focussed: i === index }) );
-		
+
 	}
 
 	// RC = Repeating Container
@@ -238,7 +245,7 @@ export class DynaformComponent implements OnInit {
 			const errorsFlat = SuperForm.getAllErrorsFlat(this.formGroup);
 			return errorsFlat;
 		}
-		return 'No Errors';
+		return false;
 	}
 
 	handleCallback(fnId: string) {
@@ -248,7 +255,7 @@ export class DynaformComponent implements OnInit {
 	private getContolKeysCSVFromMetadata(metadata): string {
 		// Return CSV of control keys in current nesting-level's metadata,
 		// excluding metadata points that don't create FormControls, FromGroups or FormArrays
-		// (identified by their 'noFormControsl' flag)
+		// (identified by their 'noFormControl' flag)
 		// e.g. ButtonGroups, HTMLChunks, etc.
 		return Object.entries(metadata)
 				.filter(([key, val]) => !(val as StringMap<any>).noFormControls)

+ 4 - 2
src/app/dynaform/dynaform.module.ts

@@ -5,12 +5,13 @@ import { RouterModule } from '@angular/router';
 
 // import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
 // import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
-import { ClarityModule } from "@clr/angular";
+import { ClarityModule } from '@clr/angular';
 
 import { DynaformComponent } from './dynaform.component';
 import { DynafieldDirective } from './directives/dynafield.directive';
 import { DynaformService } from './services/dynaform.service';
 import { ModelMapperService } from './services/model-mapper.service';
+import { FriendlyValidationErrorsService } from './services/friendly-validation-errors.service';
 
 import { ffcArr } from './components'; // ffcArr = Form Field Components Array, exported from components/index.ts
 
@@ -32,7 +33,8 @@ import { ffcArr } from './components'; // ffcArr = Form Field Components Array,
 	entryComponents: ffcArr,
 	providers: [
 		DynaformService,
-		ModelMapperService
+		ModelMapperService,
+		FriendlyValidationErrorsService
 	],
 	exports: [
 		FormsModule,

+ 2 - 0
src/app/dynaform/index.ts

@@ -3,4 +3,6 @@ export { DynaformComponent } from './dynaform.component';
 export { DynaformService } from './services/dynaform.service';
 export { ModelMapperService } from './services/model-mapper.service';
 export { standardModifiers, standardTransformer, toArrTag, arrayPad, arrayToMeta, excludeFields } from './utils';
+export { FRIENDLY_VALIDATION_ERRORS } from './config/validation-messages.config';
+export { FriendlyValErrors } from './interfaces';
 

+ 11 - 0
src/app/dynaform/interfaces/index.ts

@@ -2,5 +2,16 @@ export interface ValueTransformer {
 	inputFn: (value: string) => { modifier: string, value: string };
 	outputFn: (modifier: string, value: string) => string;
 }
+export interface FriendlyValErrors {
+	'required': string;
+	'requiredtrue': string;
+	'min': string;
+	'max': string;
+	'minlength': string;
+	'maxlength': string;
+	'email': string;
+	'pattern': string;
+}
 
 export const a = 1;
+

+ 12 - 14
src/app/dynaform/models/field.model.ts

@@ -251,18 +251,15 @@ class CheckbuttonGroup extends CheckboxGroup {
 }
 
 // ---------------------------------------------------------------------------------------------------------------------
-// Concrete Implementations - Kendo Form Components
+// Concrete Implementations
 
-class TimepickerField extends SimpleField {
-	type = 'Timepicker';
+class DatetimeField extends SimpleField {
+	type = 'Datetime';
 	value: Date | string;
-	format = 'hh:mm a';
-	steps = { hour: 1, minute: 15 };
 	constructor(meta) {
 		super(meta);
 		if (typeof this.value === 'string') {
-			const [hh, mm, ss] = this.value.split(':');
-			this.value = new Date(2000, 6, 1, +hh || 0, +mm || 0, +ss || 0);
+			this.value = new Date(this.value);
 		}
 		if (!(this.value instanceof Date)) {
 			this.value = new Date();
@@ -290,6 +287,8 @@ class Container {
 	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') {
@@ -321,7 +320,7 @@ class RepeatingContainer {
 		if (!containerMeta.initialRepeat) {
 			this.initialRepeat = containerMeta.meta.length;
 		}
-		this.meta = containerMeta.meta.map((m, i) => new Container({ 
+		this.meta = containerMeta.meta.map((m, i) => new Container({
 			name: `${this.prefix}${i+1}`,
 			meta: m,
 			button: unCamelCase(`${this.prefix}${i+1}`),
@@ -376,14 +375,13 @@ class Heading {
 }
 
 // ---------------------------------------------------------------------------------------------------------------------
-// Display
+// Display - displays non-editable vaklues lie a text field (but no input)
 
-class DisplayField {
+class DisplayField extends SimpleField {
 	value: string;
 	link?: ILink;
-	readonly noFormControls = true; // Indicates this has no FormControls associated with it
 	constructor(meta) {
-		Object.assign(this, meta);
+		super(meta);
 	}
 }
 
@@ -399,11 +397,11 @@ export {
 
 // Classes
 export {
-	SimpleField,
+	SimpleField, Option,
 	TextField, TextareaField, PasswordField, SelectField, RadioField, CheckboxField, HiddenField,
 	CheckbuttonField, DropdownModifiedInputField, MultilineField,
 	CheckboxGroup, CheckbuttonGroup,
-	TimepickerField, DatepickerField,
+	DatetimeField, DatepickerField,
 	Container, RepeatingContainer,
 	ButtonGroup, Heading, DisplayField
 };

+ 140 - 22
src/app/dynaform/services/_formdata-utils.ts

@@ -22,7 +22,7 @@
  *
  */
 
-import { FormBuilder, FormGroup, FormArray, FormControl, AbstractControlOptions } from '@angular/forms';
+import { FormBuilder, FormGroup, FormArray, FormControl, AbstractControl, AbstractControlOptions } from '@angular/forms';
 import { cloneDeep, omit, reduce } from 'lodash/fp';
 import * as fmdModels from '../models/field.model';
 
@@ -93,7 +93,7 @@ const combineExtraMeta = (metaG, extraMeta, createFromExtra = false, containerSe
 					)
 				};
 				combinedMeta[key] = combineMetaForField(metaFoG, {}, extra);
-				
+
 				// Stash a 'conbtainer template' for adding extra containers to the repeating container
 				combinedMeta[key].__containerTemplate = combineExtraMeta(
 					cloneDeep(baseObjWithAllKeys),
@@ -138,7 +138,7 @@ const generateRepeatedGroup = (metaFoG, extraMeta, baseObjWithAllKeys): StringMa
 	metaFoG.meta = metaFoG.meta.map( rcMem => ({ ...rcMem, meta: { ...baseObjWithAllKeys, ...rcMem.meta } }) ); // Add extra keys to model meta
 
 	// Extend repeated group from model (if any) to correct length, and add any missing names
-	const repeatedGroup = repeatInAutoMeta ? 
+	const repeatedGroup = repeatInAutoMeta ?
 		[ ...metaFoG.meta, ...Array(repeat - repeatInAutoMeta).fill({ meta: baseObjWithAllKeys }) ] :
 		Array(repeat).fill({ meta: baseObjWithAllKeys });
 	const fullyNamedRepeatedGroup = repeatedGroup.map((rgMem, i) => rgMem.name ? rgMem : { name: `group${i + 1}`, ...rgMem });
@@ -179,7 +179,7 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
 		}
 		return 'text';
 	}
-	
+
 	const buildFieldClassName = (t: string): string => {
 		const start = t[0].toUpperCase() + t.slice(1);
 		if (start === 'Container' || start === 'RepeatingContainer' || start === 'Heading' || t.slice(-5) === 'Group') {
@@ -187,7 +187,7 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
 		}
 		return start + 'Field';
 	};
-	
+
 	const buildModeledField = (metaFoG) => {
 		const type = resolveType(metaFoG);
 		const className = buildFieldClassName(type);
@@ -196,7 +196,7 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
 		}
 		return new fmdModels[className](metaFoG, context);
 	};
-	
+
 	// Build Form Group Member
 	const buildModeledFieldGroupMember = (metaFoG) => {
 		const modeledGroupMember = buildModeledField(metaFoG);
@@ -213,11 +213,11 @@ const buildFieldSpecificMetaInClosure = (metaG, context) => {
 		}
 		return modeledGroupMember;
 	};
-	
+
 	// Build Form Group
 	const buildModeledFieldGroupReducerIteree = (res, metaFoG) => ({ ...res, [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
 	const _buildFieldSpecificMeta = metaG => isRepeatingContainer(metaG) ?
-		metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) : 
+		metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) :
 		reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
 	const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
 
@@ -355,36 +355,51 @@ const extractFieldMappings = (metaG, parentPath = '') => Object.entries(metaG)
 // returns a function to build FormGroups containing FormControls, FormArrays and other FormGroups
 // ---------------------------------------------------------------------------------------------------------------------
 
+
+// TODO: In progress: elegantly adding validators at FormGroup and FormArray levels
+// Working, but code needs a rework
+
 const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup => {
 	// Establishes a closure over the supplied FormBuilder and returns a function that builds FormGroups from metadata
 	// ( it's done this way so we can use the FormBuilder singleton without reinitialising )
 
 	// Build Form Control
 	const buildControlState = metaF => ({ value: metaF.value || metaF.default, disabled: metaF.disabled });
-	const buildValidators = (metaF): AbstractControlOptions => ({
-		validators: metaF.validators,
-		asyncValidators: metaF.asyncValidators,
+	const buildValidators = (metaFoG): AbstractControlOptions => ({
+		validators: metaFoG.validators,
+		asyncValidators: metaFoG.asyncValidators,
 		// blur not working for custom components, so use change for custom and blur for text
-		updateOn: metaF.type === 'text' || metaF.type === 'textarea' ? 'blur' : 'change'
+		updateOn: getUpdateOn(metaFoG.type)
 	});
+	const BVAL = metaFoG => {
+		if (!metaFoG || !(metaFoG.validators || metaFoG.asyncValidators)) {
+			return undefined;
+		}
+		// console.log(metaFoG);
+		const res = buildValidators(metaFoG);
+		// console.log(res);
+		return res;
+	}
 	const buildFormControl = metaF => new FormControl(buildControlState(metaF), buildValidators(metaF));
 
 	// Build Form Array containing either Form Controls or Form Groups
-	const buildFormArray = (metaG): FormArray => {
+	const buildFormArray = (metaG, grMeta?): FormArray => {
 		return fb.array(
-			metaG.map(m => isContainer(m) ? _buildFormGroup(m.meta) : buildFormControl(m))
+			metaG.map(m => isContainer(m) ? _buildFormGroup(m.meta) : buildFormControl(m)),
+			buildValidators(grMeta)
 		);
 	};
 
-	// Build Form Group Member - builds a FormControl, FormArray or another FormGroup - which in turn can contain any of these
+	// Build Form Group Member
+	// Builds a FormControl, FormArray or another FormGroup - which in turn can contain any of these
 	const buildFormGroupMember = metaFoG => isGroup(metaFoG) ?
-		(isArray(metaFoG.meta) ? buildFormArray(metaFoG.meta) : _buildFormGroup(metaFoG.meta)) :
+		(isArray(metaFoG.meta) ? buildFormArray(metaFoG.meta, metaFoG) : _buildFormGroup(metaFoG.meta, metaFoG)) : // TODO: STINKY! REWORK with 1 param
 		buildFormControl(metaFoG);
 
 	const buildFormGroupReducerIteree = (res, metaFoG) => {
 		return metaFoG.noFormControls ? res : { ...res, [metaFoG.name]: buildFormGroupMember(metaFoG) };
 	};
-	const _buildFormGroup = _metaG => fb.group(reduce(buildFormGroupReducerIteree, {}, _metaG));
+	const _buildFormGroup = (_metaG, grMeta?) => fb.group(reduce(buildFormGroupReducerIteree, {}, _metaG), BVAL(grMeta));
 
 	// The main function - builds FormGroups containing other FormGroups, FormArrays and FormControls
 	const buildFormGroup = metaG => {
@@ -398,6 +413,72 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
 	return buildFormGroup;
 };
 
+// Get the 'update on' strategy for validators
+const getUpdateOn = (type: string): 'blur'|'change'|'submit' => {
+	const t = type.toLowerCase();
+	let res;
+	if (t === 'text' || t === 'textarea' || t === 'password') {
+		res = 'blur';
+	} else {
+		res = 'change';
+	}
+	return res;
+};
+
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Touch and update the validity of all controls in a FormGroup or FormArray / Reset validity of all controls
+// useful for displaying validation failures on submit
+// ---------------------------------------------------------------------------------------------------------------------
+
+const touchAndUpdateValidityRecursive = (group: FormGroup | FormArray): void => {
+	group.markAsTouched();
+	group.updateValueAndValidity();
+	for (const key in group.controls) {
+		if (group.controls[key] instanceof FormControl) {
+			group.controls[key].markAsTouched();
+			group.controls[key].updateValueAndValidity();
+		} else {
+			touchAndUpdateValidityRecursive(group.controls[key]);
+		}
+	}
+};
+
+const resetValidityRecursive = (group: FormGroup | FormArray): void => {
+	group.markAsUntouched();
+	group.updateValueAndValidity();
+	for (const key in group.controls) {
+		if (group.controls[key] instanceof FormControl) {
+			group.controls[key].markAsUntouched();
+			group.controls[key].updateValueAndValidity();
+		} else {
+			resetValidityRecursive(group.controls[key]);
+		}
+	}
+};
+
+const advanceUpdateStategyOfInvalidControlsRecursive = (group: FormGroup | FormArray): void => {
+	// For all invalid text / select / password control, change the updateOn startegy from 'blur' to 'change'
+	// For as-you-type feedback if validation has failed once
+	// NB: Only for the syncronous validators
+	for (const key in group.controls) {
+		if (group.controls[key] instanceof FormControl) {
+			console.log(key, group.controls[key].touched, group.controls[key].invalid);
+			if (group.controls[key].touched && group.controls[key].invalid) {
+				console.log('Replacing control', key);
+				const newControl = new FormControl(
+					group.controls[key].value,
+					{ updateOn: 'change', validators: group.controls[key].validator },
+					group.controls[key].asyncValidator
+				);
+				(group as any).setControl(key as any, newControl);
+			}
+		} else {
+			advanceUpdateStategyOfInvalidControlsRecursive(group.controls[key]);
+		}
+	}
+};
+
 
 // ---------------------------------------------------------------------------------------------------------------------
 // Reordering ( support for before / after instructions in metadata )
@@ -463,16 +544,51 @@ const generateNewModel = (originalModel, updates) => {
 	return updateObject(originalModel, updates);
 };
 
+const updateMeta = (newMeta: StringMap<any>, path: string, meta: StringMap<any>): StringMap<any> => {
+	// TODO: Finish this later
+	if (path === '/') {
+		const updatedMeta = updateObject(meta || this.meta, newMeta, true);
+		return updatedMeta;
+	}
+	// Drill down and update the branch specified by 'path' - all but final segment indicates a container
+	// What about array types? Think about this later!
+	// What about group components. Think about this later.
+	// What about bad paths. Thinkl about this later.
+	console.log(path);
+	const segments = path.split('.');
+	console.log(segments);
+	let branchMeta = meta;
+	while(isContainer(branchMeta)) {
+		const s = segments.shift();
+		console.log(s, branchMeta[s].meta);
+		// TODO: add array check
+		branchMeta = branchMeta[s].meta;
+	}
+	while(segments.length > 1) {
+		const s = segments.shift();
+		console.log(s, branchMeta[s]);
+		// TODO: add array check
+		branchMeta = branchMeta[s];
+	}
+	branchMeta = branchMeta[segments[0]];
+	console.log(segments[0], branchMeta);
+
+	// Then something like...
+	const updatedMeta = updateObject(branchMeta, newMeta, true);
+	branchMeta = updatedMeta;
+	console.log(branchMeta);
+	return meta;
+}
+
 const updateObject = (obj, updates, createAdditionalKeys = false) => {
 	// THIS DOES NOT MUTATE obj, instead returning a new object
 	if (!isRealObject(obj)) {
 		obj = {};
 	}
-	console.log('obj is', obj, typeof obj);
 	if (Object.keys(obj).length === 0) {
 		createAdditionalKeys = true;
 	}
-	const shallowClone = { ...obj };
+	const shallowClone = { ...obj }; // This might be inefficient - consider using immutable or immer
 	Object.entries(updates).forEach(([key, val]) => safeSet(shallowClone, key, val, createAdditionalKeys));
 	return shallowClone;
 };
@@ -488,7 +604,7 @@ const safeSet = (obj, key, val, createAdditionalKeys = false) => {
 	}
 
 	if (undefinedNullOrScalar(currentVal)) {
-		console.log('safeSet undefinedNullOrScalar', key, val);
+		// console.log('safeSet undefinedNullOrScalar', key, val);
 		obj[key] = val;
 	} else {
 		if (Array.isArray(currentVal)) {
@@ -576,6 +692,8 @@ const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
 
 export {
 	autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
-	buildFieldSpecificMetaInClosure, extractFieldMappings, buildFormGroupFunctionFactory,
-	generateNewModel
+	buildFieldSpecificMetaInClosure, extractFieldMappings,
+	buildFormGroupFunctionFactory,
+	touchAndUpdateValidityRecursive, resetValidityRecursive, advanceUpdateStategyOfInvalidControlsRecursive,
+	generateNewModel, updateMeta,
 };

+ 54 - 1
src/app/dynaform/services/dynaform.service.ts

@@ -65,6 +65,12 @@
  * buildNewModel
  *
  *
+ * VALIDATION
+ * ----------
+ * getValidationErrors  - get all the validation erros
+ * updateValidity		- mark all controls as touched, and update their validity state
+ * resetValidity		- mark all controls as pristine, and remove all validation failure messages
+ *
  * LOWER-LEVEL METHODS
  * -------------------
  *
@@ -93,13 +99,20 @@
 
 import { Injectable, ComponentRef } from '@angular/core';
 import { FormBuilder, FormGroup } from '@angular/forms';
+import { SuperForm } from 'angular-super-validator';
 import { ModelMapperService } from './model-mapper.service';
 
 import {
 	autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
-	buildFieldSpecificMetaInClosure, extractFieldMappings, buildFormGroupFunctionFactory, generateNewModel
+	buildFieldSpecificMetaInClosure, extractFieldMappings,
+	buildFormGroupFunctionFactory,
+	touchAndUpdateValidityRecursive, resetValidityRecursive, advanceUpdateStategyOfInvalidControlsRecursive,
+	generateNewModel, updateMeta
 } from './_formdata-utils';
 
+import { Option } from './../models/field.model';
+
+
 export interface IFormAndMeta {
 	form: FormGroup;
 	meta: StringMap<any>;
@@ -178,6 +191,14 @@ export class DynaformService {
 		(form || this.form).patchValue(mappedModel);
 	}
 
+	updateMeta(newMeta: StringMap<any>, path: string = '/', meta?: StringMap<any>): StringMap<any> {
+		const newFullMeta = updateMeta(newMeta, path, meta || this.meta);
+		if (!meta) {
+			this.meta = newFullMeta;
+		}
+		return newFullMeta;
+	}
+
 	buildNewModel(originalModel: StringMap<any>, formVal?: StringMap<any>, meta?: StringMap<any>): StringMap<any> {
 		console.log('%c *** buildNewModel *** ', this.conGreen);
 		const mapping = extractFieldMappings(meta || this.meta); // Memoize
@@ -188,6 +209,27 @@ export class DynaformService {
 		return generateNewModel(originalModel, updates);
 	}
 
+	getValidationErrors(form?: FormGroup): StringMap<string> | false {
+		const _form = form || this.form;
+		if (!_form.valid) {
+			const errorsFlat = SuperForm.getAllErrorsFlat(_form);
+			return errorsFlat;
+		}
+		return false;
+	}
+
+	updateValidity(form?: FormGroup): void {
+		touchAndUpdateValidityRecursive(form || this.form);
+	}
+
+	resetValidity(form?: FormGroup): void {
+		resetValidityRecursive(form || this.form);
+	}
+
+	goRealTime(form?: FormGroup): void {
+		advanceUpdateStategyOfInvalidControlsRecursive(form || this.form);
+	}
+
 	// -----------------------------------------------------------------------------------------------------------------
 	// Convenience methods combining several steps
 
@@ -242,5 +284,16 @@ export class DynaformService {
 		return autoMeta(model);
 	}
 
+	// -----------------------------------------------------------------------------------------------------------------
+	// Convenience methods
+
+	buildOptions(options): Option[] {
+		if (Array.isArray(options)) {
+			return options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
+		} else {
+			throw new Error('Options must be supplied as an array');
+		}
+	}
+
 }
 

+ 53 - 0
src/app/dynaform/services/friendly-validation-errors.service.ts

@@ -0,0 +1,53 @@
+import { Injectable, Inject, Optional } from '@angular/core';
+import { FRIENDLY_VALIDATION_ERRORS, friendlyValidationErrors } from './../config/validation-messages.config';
+import { FriendlyValErrors } from './../interfaces';
+import { path } from 'ramda';
+
+
+@Injectable()
+export class FriendlyValidationErrorsService {
+
+	fErrs: FriendlyValErrors;
+
+	ident;
+
+	constructor(@Inject(FRIENDLY_VALIDATION_ERRORS) fErrs: FriendlyValErrors) {
+		this.ident = new Date().getTime();
+		console.log(this.ident, this.fErrs);
+		if (!this.fErrs) {
+			this.fErrs = friendlyValidationErrors;
+		}
+	}
+
+	getFriendly(errorType: string, data?: StringMap<string|number>): string {
+		const friendlyError = this.fErrs[errorType];
+		if (friendlyError) {
+			const dataNotEmpty = typeof data === 'object' && data !== null && Object.keys(data).length;
+			return dataNotEmpty ? this.processTemplateSubstitutions(friendlyError, data) : friendlyError;
+		}
+		return errorType;
+	}
+
+	// -----------------
+	// Simple templating
+
+	private processTemplateSubstitutions(urlTemplate: string, data: StringMap<string|number>): string {
+		const tokens = this.getSubstitutionTokens(urlTemplate);
+		const processedUrl = tokens.reduce(
+			(str, token) => str.replace(new RegExp(`{{\\s${token}\\s+}}`), path(token.split('.'), data)),
+			urlTemplate
+		);
+		return processedUrl;
+	}
+
+	private getSubstitutionTokens(urlTemplate: string): string[] {
+		const resArr = [];
+		let tempArr;
+		const regex = new RegExp('{{\\s+(.*?)\\s+}}', 'g');
+		while ((tempArr = regex.exec(urlTemplate)) !== null) {
+			resArr.push(tempArr[1]);
+		}
+		return resArr;
+	}
+
+}

+ 3 - 3
src/app/dynaform/testdata/testset.1.ts

@@ -127,8 +127,8 @@ in the house`
 // 	value: "12:45"
 // };
 
-const clrdatepicker = {
-	type: 'clrDatepicker'
+const datepicker = {
+	type: 'datepicker'
 };
 
 // ---------------------------------------------------------------------------------------------------------------------
@@ -175,7 +175,7 @@ const meta = {
 	dropdownModifiedInput,
 	checkbuttonGroup,
 	// timepicker,
-	clrdatepicker,
+	datepicker,
 	container,
 	multiline
 };

+ 29 - 0
src/app/dynaform/testdata/testset.5.ts

@@ -0,0 +1,29 @@
+import { Validators } from '@angular/forms';
+import { ValueTransformer } from './../interfaces';
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Clarity
+
+// const timepicker = {
+// 	type: 'timepicker',
+// 	// value: new Date(1999, 10, 11, 12, 30, 45)
+// 	value: "12:45"
+// };
+
+const datepicker = {
+	type: 'datepicker'
+};
+
+const datetime = {
+	type: 'datetime'
+};
+
+// ---------------------------------------------------------------------------------------------------------------------
+
+const model = {};
+
+const meta = {
+	datetime
+};
+
+export { model, meta };