ソースを参照

Cherrypicking improvements from clarity branch

Richard Knight 6 年 前
コミット
53c1649a2b
共有51 個のファイルを変更した1058 個の追加701 個の削除を含む
  1. 405 491
      package-lock.json
  2. 24 24
      package.json
  3. 5 0
      src/app/_mock/testfields.v1.ts
  4. 2 2
      src/app/app.component.ts
  5. 3 3
      src/app/app.module.ts
  6. 1 1
      src/app/dynaform/components/_abstract/group-input.component.ts
  7. 8 1
      src/app/dynaform/components/_abstract/native-input.component.ts
  8. 5 0
      src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.html
  9. 0 0
      src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.scss
  10. 25 0
      src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.spec.ts
  11. 21 0
      src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.ts
  12. 3 0
      src/app/dynaform/components/clarity/datepicker/datepicker.component.html
  13. 0 0
      src/app/dynaform/components/clarity/datepicker/datepicker.component.scss
  14. 25 0
      src/app/dynaform/components/clarity/datepicker/datepicker.component.spec.ts
  15. 24 0
      src/app/dynaform/components/clarity/datepicker/datepicker.component.ts
  16. 3 0
      src/app/dynaform/components/clarity/password/clr-password.component.html
  17. 0 0
      src/app/dynaform/components/clarity/password/clr-password.component.scss
  18. 25 0
      src/app/dynaform/components/clarity/password/clr-password.component.spec.ts
  19. 13 0
      src/app/dynaform/components/clarity/password/clr-password.component.ts
  20. 7 0
      src/app/dynaform/components/clarity/radio/clr-radio.component.html
  21. 0 0
      src/app/dynaform/components/clarity/radio/clr-radio.component.scss
  22. 25 0
      src/app/dynaform/components/clarity/radio/clr-radio.component.spec.ts
  23. 19 0
      src/app/dynaform/components/clarity/radio/clr-radio.component.ts
  24. 17 0
      src/app/dynaform/components/clarity/select/clr-select.component.html
  25. 0 0
      src/app/dynaform/components/clarity/select/clr-select.component.scss
  26. 25 0
      src/app/dynaform/components/clarity/select/clr-select.component.spec.ts
  27. 24 0
      src/app/dynaform/components/clarity/select/clr-select.component.ts
  28. 16 0
      src/app/dynaform/components/clarity/text/clr-text.component.html
  29. 0 0
      src/app/dynaform/components/clarity/text/clr-text.component.scss
  30. 25 0
      src/app/dynaform/components/clarity/text/clr-text.component.spec.ts
  31. 13 0
      src/app/dynaform/components/clarity/text/clr-text.component.ts
  32. 5 0
      src/app/dynaform/components/clarity/textarea/clr-textarea.component.html
  33. 0 0
      src/app/dynaform/components/clarity/textarea/clr-textarea.component.scss
  34. 25 0
      src/app/dynaform/components/clarity/textarea/clr-textarea.component.spec.ts
  35. 13 0
      src/app/dynaform/components/clarity/textarea/clr-textarea.component.ts
  36. 18 9
      src/app/dynaform/components/custom/checkbutton/checkbutton.component.ts
  37. 2 2
      src/app/dynaform/components/nocontrol/button-group/button-group.component.ts
  38. 2 2
      src/app/dynaform/components/nocontrol/display/display.component.ts
  39. 1 1
      src/app/dynaform/components/nocontrol/heading/heading.component.ts
  40. 2 2
      src/app/dynaform/directives/dynafield.directive.ts
  41. 7 4
      src/app/dynaform/dynaform.component.html
  42. 7 7
      src/app/dynaform/dynaform.component.ts
  43. 1 2
      src/app/dynaform/dynaform.module.ts
  44. 57 28
      src/app/dynaform/models/field.model.ts
  45. 79 62
      src/app/dynaform/services/_formdata-utils.ts
  46. 36 13
      src/app/dynaform/services/dynaform.service.ts
  47. 32 32
      src/app/dynaform/services/model-mapper.service.ts
  48. 1 1
      src/app/dynaform/utils.ts
  49. 0 13
      src/app/ng-dynaform.code-workspace
  50. 6 0
      src/styles.scss
  51. 1 1
      src/typings.d.ts

ファイルの差分が大きいため隠しています
+ 405 - 491
package-lock.json


+ 24 - 24
package.json

@@ -12,49 +12,49 @@
 	},
 	"private": true,
 	"dependencies": {
-		"@angular/animations": "^7.2.0",
-		"@angular/common": "^7.2.0",
-		"@angular/compiler": "^7.2.0",
-		"@angular/core": "^7.2.0",
-		"@angular/forms": "^7.2.0",
-		"@angular/http": "^7.2.0",
-		"@angular/platform-browser": "^7.2.0",
-		"@angular/platform-browser-dynamic": "^7.2.0",
-		"@angular/router": "^7.2.0",
-		"@ng-bootstrap/ng-bootstrap": "^4.0.1",
-		"@progress/kendo-angular-dateinputs": "^3.5.3",
+		"@angular/animations": "^7.2.2",
+		"@angular/common": "^7.2.2",
+		"@angular/compiler": "^7.2.2",
+		"@angular/core": "^7.2.2",
+		"@angular/forms": "^7.2.2",
+		"@angular/http": "^7.2.2",
+		"@angular/platform-browser": "^7.2.2",
+		"@angular/platform-browser-dynamic": "^7.2.2",
+		"@angular/router": "^7.2.2",
+		"@ng-bootstrap/ng-bootstrap": "^4.0.2",
+		"@progress/kendo-angular-dateinputs": "^3.5.4",
 		"@progress/kendo-angular-intl": "^1.6.1",
 		"@progress/kendo-angular-l10n": "^1.3.0",
-		"@progress/kendo-theme-bootstrap": "^3.0.0",
+		"@progress/kendo-theme-bootstrap": "^3.2.0",
 		"angular-super-validator": "^2.0.0",
-		"core-js": "^2.6.2",
+		"core-js": "^2.6.3",
 		"json-formatter-js": "^2.2.1",
 		"lodash": "^4.17.11",
 		"rxjs": "^6.3.3",
 		"rxjs-compat": "^6.3.3",
-		"zone.js": "^0.8.27"
+		"zone.js": "^0.8.29"
 	},
 	"devDependencies": {
-		"@angular-devkit/build-angular": "~0.12.1",
-		"@angular/cli": "~7.2.1",
-		"@angular/compiler-cli": "^7.2.0",
-		"@angular/language-service": "^7.2.0",
-		"@types/jasmine": "~3.3.5",
+		"@angular-devkit/build-angular": "~0.12.3",
+		"@angular/cli": "~7.2.3",
+		"@angular/compiler-cli": "^7.2.2",
+		"@angular/language-service": "^7.2.2",
+		"@types/jasmine": "~3.3.8",
 		"@types/jasminewd2": "~2.0.6",
-		"@types/lodash": "^4.14.119",
-		"@types/node": "~10.12.18",
+		"@types/lodash": "^4.14.120",
+		"@types/node": "~10.12.19",
 		"codelyzer": "^4.5.0",
 		"jasmine-core": "~3.3.0",
 		"jasmine-spec-reporter": "~4.2.1",
-		"karma": "~3.1.4",
+		"karma": "~4.0.0",
 		"karma-chrome-launcher": "~2.2.0",
 		"karma-coverage-istanbul-reporter": "^2.0.4",
 		"karma-jasmine": "~2.0.1",
 		"karma-jasmine-html-reporter": "^1.4.0",
 		"protractor": "^5.4.2",
 		"rxjs-tslint": "^0.1.6",
-		"ts-node": "~7.0.1",
+		"ts-node": "~8.0.2",
 		"tslint": "~5.12.1",
-		"typescript": "3.2.2"
+		"typescript": "3.2.4"
 	}
 }

+ 5 - 0
src/app/_mock/testfields.v1.ts

@@ -39,6 +39,11 @@ const radioField = new fmd.RadioField({
 	options: ['Tea', 'Coffee', 'Cocoa', 'Yerba Maté']
 });
 
+const checkboxField = new fmd.CheckboxField({
+	name: 'checkboxField',
+	checkedValue: 'Howdy Checkbox',
+});
+
 const disabledTextField = new fmd.TextField({
 	name: 'disabledTextField',
 	placeholder: 'You can\'t touch this',

+ 2 - 2
src/app/app.component.ts

@@ -17,7 +17,7 @@ import * as test12 from './_mock/testfields.v12';
 
 const testdata = [ test1, test2, test3, test4, test5, test6, test7, test8, test9, test10, test11, test12 ];
 
-const defatltTest = 12;
+const defatltTest = 1;
 
 @Component({
 	selector: 'app-root',
@@ -27,7 +27,7 @@ const defatltTest = 12;
 export class AppComponent implements OnInit, OnChanges {
 
 	form: FormGroup;
-	meta: StringMap;
+	meta: StringMap<any>;
 
 	hCssRed = 'color: white; background-color: maroon; font-weight: bold;';
 	hCssGreen = 'color: white; background-color: green; font-weight: bold;';

+ 3 - 3
src/app/app.module.ts

@@ -1,4 +1,4 @@
-import { NgModule } from '@angular/core';
+import { NgModule, LOCALE_ID } from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -23,7 +23,7 @@ import { JsonFormatterDirective } from './directives/json-formatter.directive';
 		AppComponent,
 		JsonFormatterDirective
 	],
-	providers: [],
-	bootstrap: [AppComponent]
+	providers: [ { provide: LOCALE_ID, useValue: 'en-gb' } ],
+	bootstrap:  [AppComponent ]
 })
 export class AppModule { }

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

@@ -10,7 +10,7 @@ export abstract class GroupInputComponent implements OnInit {
 	meta;
 
 	formGroup: FormGroup;
-	childMetaArray: Array<StringMap>;
+	childMetaArray: Array<StringMap<any>>;
 	controlNames: Array<string>;
 
 	exposeMetaInTemplate: string[] = [];

+ 8 - 1
src/app/dynaform/components/_abstract/native-input.component.ts

@@ -1,4 +1,4 @@
-import { Input, OnInit } from '@angular/core';
+import { Input, Output, EventEmitter, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 
 export abstract class NativeInputComponent implements OnInit {
@@ -9,6 +9,9 @@ export abstract class NativeInputComponent implements OnInit {
 	@Input()
 	meta;
 
+	@Output()
+	call: EventEmitter<string> = new EventEmitter<string>();
+
 	exposeMetaInTemplate: string[] = [];
 
 	ngOnInit() {
@@ -16,4 +19,8 @@ export abstract class NativeInputComponent implements OnInit {
 		this.exposeMetaInTemplate.map(p => this[p] = this.meta[p] !== undefined ? this.meta[p] : this[p]);
 	}
 
+	handle(fnId: string, val: any) {
+		this.call.emit(fnId);
+	}
+
 }

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

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

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


+ 25 - 0
src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.spec.ts

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

+ 21 - 0
src/app/dynaform/components/clarity/checkbox/clr-checkbox.component.ts

@@ -0,0 +1,21 @@
+import { Component } from '@angular/core';
+import { NativeInputComponent } from '../../_abstract/native-input.component';
+
+
+@Component({
+	selector: 'app-checkbox',
+	templateUrl: './clr-checkbox.component.html',
+	styleUrls: ['./clr-checkbox.component.scss']
+})
+export class ClrCheckboxComponent extends NativeInputComponent {
+
+	exposeMetaInTemplate: string[] = ['label'];
+
+	setValue(cb: HTMLInputElement) {
+		this.control.setValue(cb.checked ? this.meta.checkedValue : false);
+		if (this.meta.change) {
+			this.handle(this.meta.change, this.control.value);
+		}
+	}
+
+}

+ 3 - 0
src/app/dynaform/components/clarity/datepicker/datepicker.component.html

@@ -0,0 +1,3 @@
+<clr-date-container [ngClass]="extraClass">
+	<input clrDate [formControl]="control">
+</clr-date-container>

+ 0 - 0
src/app/dynaform/components/clarity/datepicker/datepicker.component.scss


+ 25 - 0
src/app/dynaform/components/clarity/datepicker/datepicker.component.spec.ts

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

+ 24 - 0
src/app/dynaform/components/clarity/datepicker/datepicker.component.ts

@@ -0,0 +1,24 @@
+import { Component } from '@angular/core';
+import { NativeInputComponent } from '../../_abstract/native-input.component';
+
+@Component({
+	selector: 'app-datepicker',
+	templateUrl: './datepicker.component.html',
+	styleUrls: ['./datepicker.component.scss']
+})
+export class ClrDatepickerComponent extends NativeInputComponent {
+
+	exposeMetaInTemplate: string[] = ['extraClass', 'placeholder'];
+
+	ngOnInit() {
+		super.ngOnInit();
+		
+		// CLarity datepicker expects a string when used reactively
+		const dateObj = this.control.value;
+		const d = dateObj.getDate();
+		const m = dateObj.getMonth();
+		const y = dateObj.getFullYear();
+		this.control.setValue(`${d}/${m + 1}/${y}`);
+	}
+
+}

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

@@ -0,0 +1,3 @@
+<clr-password-container>
+    <input clrPassword [formControl]="control" [placeholder]="placeholder" />
+</clr-password-container>

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


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

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

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

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

+ 7 - 0
src/app/dynaform/components/clarity/radio/clr-radio.component.html

@@ -0,0 +1,7 @@
+<div class="clr-radio-container" [ngClass]="{'clr-control-inline' : horizontal}">
+	<label class="small">{{ 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>
+</div>

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


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

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RadioComponent } from './clr-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();
+  });
+});

+ 19 - 0
src/app/dynaform/components/clarity/radio/clr-radio.component.ts

@@ -0,0 +1,19 @@
+import { Component } from '@angular/core';
+import { NativeInputComponent } from '../../_abstract/native-input.component';
+
+@Component({
+	selector: 'app-radio',
+	templateUrl: './clr-radio.component.html',
+	styleUrls: ['./clr-radio.component.scss']
+})
+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();
+	}
+
+}

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

@@ -0,0 +1,17 @@
+<clr-select-container *ngIf="!link; else fieldWithLink">
+	<label>{{ label }}</label>
+	<select clrSelect [formControl]="control">
+		<option *ngFor="let opt of options" [value]="opt.value">{{ opt.label }}</option>
+	</select>
+</clr-select-container>
+
+<ng-template #fieldWithLink>
+	<clr-select-container class="clr-input-group clr-input-group-sm">
+		<select [formControl]="control" clrSelect #field>
+			<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-select-container>
+</ng-template>

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


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

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SelectComponent } from './clr-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();
+  });
+});

+ 24 - 0
src/app/dynaform/components/clarity/select/clr-select.component.ts

@@ -0,0 +1,24 @@
+import { Component } from '@angular/core';
+import { NativeInputComponent } from '../../_abstract/native-input.component';
+import { Router, ActivatedRoute } from '@angular/router';
+
+@Component({
+	selector: 'app-select',
+	templateUrl: './clr-select.component.html',
+	styleUrls: ['./clr-select.component.scss']
+})
+export class ClrSelectComponent extends NativeInputComponent {
+
+	exposeMetaInTemplate: string[] = ['label', 'options', 'link'];
+
+	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 });
+	}
+
+}

+ 16 - 0
src/app/dynaform/components/clarity/text/clr-text.component.html

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

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


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

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

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

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

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

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

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


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

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

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

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

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

@@ -1,4 +1,4 @@
-import { Component, forwardRef, OnChanges } from '@angular/core';
+import { Component, OnChanges, forwardRef, ChangeDetectorRef } from '@angular/core';
 import { NG_VALUE_ACCESSOR } from '@angular/forms';
 import { CustomInputComponent } from './../../_abstract/custom-input.component';
 
@@ -18,20 +18,27 @@ export class CheckbuttonComponent extends CustomInputComponent implements OnChan
 
 	exposeMetaInTemplate: string[] = ['label', 'value', 'disabled', 'checkedValue', 'onChange'];
 
+	label: string;
 	value?: string | boolean = true;
 	isChecked: boolean;
 	disabled = false;
-	currentValue: string | boolean;
-	checkedValue: string | boolean = true;
-	onChange: (val) => {};
+	currentValue: string | number | boolean;
+	checkedValue: string | number | boolean = true;
+	onChange: (val) => void;
+
+	constructor(private _cdr: ChangeDetectorRef) {
+		super();
+	}
 
 	ngOnChanges() {
 		this.disabled = this.meta.disabled;
 	}
 
-	private toggleChecked(e): void {
-		e.target.blur();
-		e.preventDefault();
+	private toggleChecked(e?): void {
+		if (e) {
+			e.target.blur();
+			e.preventDefault();
+		}
 		if (this.disabled) { return; }
 		this.isChecked = !this.isChecked;
 		this.currentValue = this.isChecked ? this.value || this.checkedValue : false;
@@ -46,7 +53,9 @@ export class CheckbuttonComponent extends CustomInputComponent implements OnChan
 	}
 
 	public writeValue(value: any): void {
-		this.isChecked = value && value !== 0 && value !== 'false' ? value : false;
-		this.currentValue = this.isChecked ? value : false;
+		value = value ? this.checkedValue : false;
+		this.isChecked = !!value;
+		this.currentValue = this.isChecked ? this.checkedValue : false;
+		this._cdr.markForCheck(); // We have to manually trigger change detection when using setValue or patchValue from outside this component
 	}
 }

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

@@ -8,12 +8,12 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
 export class ButtonGroupComponent implements OnInit {
 
 	@Input()
-	meta: StringMap;
+	meta: StringMap<any>;
 
 	@Output()
 	call: EventEmitter<string> = new EventEmitter<string>();
 
-	buttons: StringMap[];
+	buttons: StringMap<any>[];
 
 	ngOnInit() {
 		this.buttons = this.meta.meta;

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

@@ -8,10 +8,10 @@ import { Component, Input, OnInit } from '@angular/core';
 export class DisplayComponent implements OnInit {
 
 	@Input()
-	meta: StringMap;
+	meta: StringMap<any>;
 
 	value: string;
-	link?: StringMap;
+	link?: StringMap<any>;
 
 	ngOnInit() {
 		this.value = this.meta.value;

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

@@ -8,7 +8,7 @@ import { Component, Input, OnInit } from '@angular/core';
 export class HeadingComponent implements OnInit {
 
 	@Input()
-	meta: StringMap;
+	meta: StringMap<any>;
 
 	text: string;
 	level: number;

+ 2 - 2
src/app/dynaform/directives/dynafield.directive.ts

@@ -14,7 +14,7 @@ import * as formFieldComponents from './../components';
 
 interface IFFC {
 	control: FormControl; // Remember, this can be an individual FormControl or a FormGroup
-	meta: StringMap;
+	meta: StringMap<any>;
 	propagateChange?: Function;
 	call?: EventEmitter<string>;
 }
@@ -30,7 +30,7 @@ const componentType = (type: string): string => type[0].toUpperCase() + type.sli
 export class DynafieldDirective extends NgControl implements OnInit, OnDestroy {
 
 	@Input()
-	meta: StringMap;
+	meta: StringMap<any>;
 
 	@Input()
 	set control(fc: FormControl) {

+ 7 - 4
src/app/dynaform/dynaform.component.html

@@ -17,7 +17,11 @@
 					></span>
 				</label>
 				<div class="col-sm-8">
-					<ng-container dynafield [control]="control" [meta]="meta" (call)="handleCallback($event)"></ng-container>
+          <ng-container dynafield
+            [control]="control"
+            [meta]="meta"
+            (call)="handleCallback($event)"
+          ></ng-container>
 				</div>
 			</div>
 		</ng-container>
@@ -64,7 +68,7 @@
 				<ng-container dynafield [control]="control" [meta]="meta" (call)="handleCallback($event)"></ng-container>
 			</div>
 		</ng-template>
-		
+
 	</ng-container>
 </ng-template>
 
@@ -84,7 +88,6 @@
 			'alert-danger' 		: !formGroup.valid && formGroup.dirty,
 			'alert-secondary' 	: !formGroup.dirty
 		}"
-		class="alert mt-4"
+		class="alert __debug mt-4"
 		role="alert">{{ formGroup.pristine ? 'Untouched' : getValidationErrors() | json }}</pre>
 </div>
-	

+ 7 - 7
src/app/dynaform/dynaform.component.ts

@@ -5,7 +5,7 @@ import { unwrapResolvedMetadata } from '@angular/compiler';
 
 export interface DynarowContext {
 	control: AbstractControl;
-	meta: StringMap;
+	meta: StringMap<any>;
 }
 
 @Component({
@@ -54,7 +54,7 @@ export class DynaformComponent implements OnInit {
 	@Output()
 	call: EventEmitter<string> = new EventEmitter<string>();
 
-	formMetaData: StringMap; // TODO: Tighten up type
+	formMetaData: StringMap<any>; // TODO: Tighten up type
 	controlNames: string[];
 	dynaFormRows: string[];
 	path: string[]; // path of current FormGroup - can be used to respond differently based on nesting level in template
@@ -124,11 +124,11 @@ export class DynaformComponent implements OnInit {
 		return path;
 	}
 
-	isField(meta: StringMap): boolean {
+	isField(meta: StringMap<any>): boolean {
 		return !meta.type.includes('Container');
 	}
 	
-	isRepeatingContainer(meta: StringMap): boolean {
+	isRepeatingContainer(meta: StringMap<any>): boolean {
 		return meta.type === 'RepeatingContainer';
 	}
 
@@ -148,7 +148,7 @@ export class DynaformComponent implements OnInit {
 		return result;
 	}
 
-	getRowClass(control: FormControl, meta: StringMap): string {
+	getRowClass(control: FormControl, meta: StringMap<any>): string {
 		const fieldTypeClass = meta.type ? meta.type.toLowerCase().replace('component', '') : '';
 		const fieldClass = Array.isArray(meta.class) ? meta.class.join(' ') : meta.class;
 		const containerClass = fieldClass ? ` container-${fieldClass}` : '';
@@ -190,7 +190,7 @@ export class DynaformComponent implements OnInit {
 
 	}
 
-	getValidationFailureMessage(control: FormControl, meta: StringMap) {
+	getValidationFailureMessage(control: FormControl, meta: StringMap<any>) {
 		if (control.errors) {
 			const errKeys = Object.keys(control.errors);
 			console.log(errKeys);
@@ -216,7 +216,7 @@ export class DynaformComponent implements OnInit {
 		// (identified by their 'noFormControsl' flag)
 		// e.g. ButtonGroups, HTMLChunks, etc.
 		return Object.entries(metadata)
-				.filter(([key, val]) => !(val as StringMap).noFormControls)
+				.filter(([key, val]) => !(val as StringMap<any>).noFormControls)
 				.reduce((acc, [key]) => [...acc, key], [])
 				.join(',');
 	}

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

@@ -38,8 +38,7 @@ import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
 		FormsModule,
 		ReactiveFormsModule,
 		DynaformComponent,
-		DynafieldDirective,
-		ffcArr
+		DynafieldDirective
 	]
 })
 export class DynaformModule { }

+ 57 - 28
src/app/dynaform/models/field.model.ts

@@ -11,21 +11,23 @@ import { standardModifiers, standardTransformer } from './../utils';
 
 interface ISimpleFieldMetaData {
 	name: string; 							// The FormControl name
-	source?: string;						// Location in API-returned model - defaults to name
-	type?: string; 							// The component type e.g. BasicInput, Checkbutton, Timepicker, etc
+	type?: string; 							// The component type e.g. Text, Checkbutton, Timepicker, etc
 	label?: string;							// The field label - defaults to unCamelCased name if not supplied
 	value?: any;							// The field value - defaults to empty string if not supplied
+	checkedValue?: boolean|number|string;	// Checkboxes and Checkbuttons only
 	default?: any;							// Default value
 	placeholder?: string;					// Optional placeholder text
 	class?: string | string[];				// CSS classes to apply
 	id?: string;							// CSS id to apply
+	disabled?: boolean;						// Whether field is initially disabled
+	change?: string;						// Name of function in host component to call when value changes
+	source?: string;						// Location in API-returned model - defaults to name
 	before?: string;						// Ordering instruction - move before <name of another key in group>
 	after?: string;							// Ordering instruction - move after <name of another key in group>
-	disabled?: boolean;					// Whether field is initially disabled
 	validators?: ValidatorFn[];				// Array of validator functions - following Angular FormControl API
 	asyncValidators?: AsyncValidatorFn[];	// Array of async validator functions - following Angular FormControl API
-	valFailureMsgs?: StringMap;				// Validation failure messages - display appropriate message if validation fails
-	onChange?: (val) => {};					// Function to call when field's value changes
+	valFailureMsgs?: StringMap<any>;		// Validation failure messages - display appropriate message if validation fails
+	// onChange?: (val) => {};				// Function to call when field's value changes
 }
 
 interface IOption {
@@ -52,7 +54,7 @@ interface IDropdownModifiedInputFieldMetaData extends ISimpleFieldMetaData {
 interface ITimePickerFieldMetaData extends ISimpleFieldMetaData {
 	value: Date | string;
 	format: string;
-	steps: StringMap;
+	steps: StringMap<any>;
 }
 
 // Utility to unCamelCase
@@ -74,19 +76,18 @@ abstract class SimpleField {
 	source?: string;
 	label?: string;
 	value;
+	checkedValue?: boolean|number|string;
 	default = '';
 	placeholder = '';
 	class?: string | string[];
 	id?: string;
 	disabled = false;
+	change?: string;
 	validators: ValidatorFn[] = [];
 	asyncValidators: AsyncValidatorFn[] = [];
-	valFailureMsgs: StringMap = {};
+	valFailureMsgs: StringMap<any> = {};
 
 	constructor(meta: ISimpleFieldMetaData) {
-		if (meta.type === 'Multiline') {
-			console.log(meta);
-		}
 		Object.assign(this, meta);
 		if (!this.source) {
 			// If source is not supplied it's the same as the name
@@ -114,7 +115,7 @@ class Option implements IOption {
 				this.label = opt.label;
 				this.value = opt.value;
 			}
-		} else if (typeof opt === 'string') {
+		} else {
 			this.label = opt;
 			this.value = opt;
 		}
@@ -123,10 +124,17 @@ class Option implements IOption {
 
 abstract class OptionsField extends SimpleField {
 	options: Option[] = [];
-	constructor(meta: IOptionsFieldMetaData) {
+	constructor(meta: IOptionsFieldMetaData, context?: any) {
 		super(meta);
-		if (Array.isArray(meta.options)) {
-			this.options = meta.options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
+		let options;
+		if (typeof meta.options === 'function') {
+			const boundFn = meta.options.bind(context);
+			options = boundFn();
+		} else {
+			options = meta.options;
+		}
+		if (Array.isArray(options)) {
+			this.options = options.reduce((acc, opt) => { acc.push(new Option(opt)); return acc; }, []);
 		} else {
 			this.options = [
 				new Option({ label: 'Yes', value: true }),
@@ -161,6 +169,26 @@ class RadioField extends OptionsField {
 	type = 'Radio';
 }
 
+class CheckboxField extends SimpleField {
+	type = 'Checkbox';
+	isChecked: boolean;
+	default: any = false;
+	checkedValue: boolean|number|string = true;
+	rowLabel: null;
+	constructor(meta: ISimpleFieldMetaData) {
+		super(meta);
+		if (meta.value) {
+			this.checkedValue = meta.value;
+		}
+		if (meta.checkedValue) {
+			this.checkedValue = meta.checkedValue;
+		}
+		if (meta.default) {
+			this.default = meta.default;
+		}
+	}
+}
+
 class HiddenField extends SimpleField {
 	type = 'Hidden';
 }
@@ -168,12 +196,8 @@ class HiddenField extends SimpleField {
 // ---------------------------------------------------------------------------------------------------------------------
 // Concrete Implementations - Custom Form Components
 
-class CheckbuttonField extends SimpleField {
+class CheckbuttonField extends CheckboxField {
 	type = 'Checkbutton';
-	default: any = false;
-	isChecked: boolean;
-	checkedValue: boolean | string = true;
-	rowLabel: null;
 }
 
 class DropdownModifiedInputField extends SimpleField {
@@ -194,8 +218,8 @@ class MultilineField extends SimpleField {
 // ---------------------------------------------------------------------------------------------------------------------
 // Concrete Implementations - Custom FormGroup Components (which render a group of FormControls)
 
-class CheckbuttonGroup {
-	type = 'CheckbuttonGroup';
+class CheckboxGroup {
+	type = 'CheckboxGroup';
 	name: string;
 	label?: string;
 	groupName: string;
@@ -222,6 +246,11 @@ class CheckbuttonGroup {
 	}
 }
 
+class CheckbuttonGroup extends CheckboxGroup {
+	type = 'CheckbuttonGroup';
+}
+
+
 // ---------------------------------------------------------------------------------------------------------------------
 // Concrete Implementations - Kendo Form Components
 
@@ -255,12 +284,12 @@ class Container {
 	type = 'Container';
 	name: string;
 	label = '';
-	seed: StringMap;
+	seed: StringMap<any>;
 	template?: TemplateRef<any>;
 	button: string;	
 	focussed: boolean = true;	
-	meta: StringMap; // TODO: Tighten up on type with union type
-	constructor(containerMeta: StringMap) {
+	meta: StringMap<any>; // TODO: Tighten up on type with union type
+	constructor(containerMeta: StringMap<any>) {
 		Object.assign(this, containerMeta);
 		if (typeof this.label === 'undefined') {
 			this.label = unCamelCase(this.name);
@@ -273,7 +302,7 @@ class RepeatingContainer {
 	name: string;
 	prefix: string = 'group';
 	label = '';
-	seed: StringMap;
+	seed: StringMap<any>;
 	template?: TemplateRef<any>;
 	meta: Container[]; // An array of Containers
 	minRepeat: number = 1;
@@ -283,7 +312,7 @@ class RepeatingContainer {
 	showDeleteControl: boolean = true;
 	primaryField: string = '';
 	display: string = 'SINGLE'; // Display strategy to use  - ALL or SINGLE - All at once, or one at a time (with a switcher)
-	constructor(containerMeta: StringMap) {
+	constructor(containerMeta: StringMap<any>) {
 		Object.assign(this, containerMeta);
 		if (typeof this.label === 'undefined') {
 			this.label = unCamelCase(this.name);
@@ -364,9 +393,9 @@ class DisplayField {
 
 export {
 	SimpleField,
-	TextField, TextareaField, PasswordField, SelectField, RadioField, HiddenField,
+	TextField, TextareaField, PasswordField, SelectField, RadioField, CheckboxField, HiddenField,
 	CheckbuttonField, DropdownModifiedInputField, MultilineField,
-	CheckbuttonGroup,
+	CheckboxGroup, CheckbuttonGroup,
 	TimepickerField, DatepickerField,
 	Container, RepeatingContainer,
 	ButtonGroup, Heading, DisplayField

+ 79 - 62
src/app/dynaform/services/_formdata-utils.ts

@@ -129,7 +129,7 @@ const combineModelWithMeta = (model, extraMeta, createFromExtra = false) => comb
 
 // <--- Utilities supporting Repreating Containers --->
 
-const generateRepeatedGroup = (metaFoG, extraMeta, baseObjWithAllKeys): StringMap[] => {
+const generateRepeatedGroup = (metaFoG, extraMeta, baseObjWithAllKeys): StringMap<any>[] => {
 	// Calculate the number of repeats
 	const repeatInAutoMeta = Array.isArray(metaFoG.meta) ? metaFoG.meta.length : 0;
 	const repeatInExtraMeta = extraMeta['initialRepeat'] || extraMeta['minRepeat'];
@@ -146,7 +146,7 @@ const generateRepeatedGroup = (metaFoG, extraMeta, baseObjWithAllKeys): StringMa
 }
 
 // Get Repeating Container Base Object With All Keys
-const getRCBaseObjectWithAllKeys = (metaFoG, extraMeta, createFromExtra = false): StringMap => {
+const getRCBaseObjectWithAllKeys = (metaFoG, extraMeta, createFromExtra = false): StringMap<any> => {
 	// If creating from extra, make sure all group members have all keys in both model and meta (as this is a repeating group)
 	const keysFromModel = isArray(metaFoG.meta) && metaFoG.meta.length ? Object.keys(metaFoG.meta[0].meta) : [];
 	const keysFromExtraMeta = extraMeta['meta'] && extraMeta['meta'][0] ? Object.keys(extraMeta['meta'][0]) : [];
@@ -160,63 +160,71 @@ const getRCBaseObjectWithAllKeys = (metaFoG, extraMeta, createFromExtra = false)
 // Build Form-Field-Type-Specific Metadata (using the field models in dynaform/models)
 // ---------------------------------------------------------------------------------------------------------------------
 
-const resolveType = (metaFoG: StringMap): string => {
-	if (metaFoG.type) {
-		return metaFoG.type;
-	}
-	if (isContainer(metaFoG)) {
-		return 'container';
-	}
-	if (isRepeatingContainer(metaFoG)) {
-		return 'repeatingContainer';
-	}
-	return 'text';
-}
+// MAYBE CHANGE INTO A MODULE SO WE CAN USE CLOSURE BUT ALSO METHODS INSIDE IT
+// More Elegant But Not Urgent
 
-const buildFieldClassName = (t: string): string => {
-	const start = t[0].toUpperCase() + t.slice(1);
-	if (start === 'Container' || start === 'RepeatingContainer' || start === 'Heading' || t.slice(-5) === 'Group') {
-		return start;
-	}
-	return start + 'Field';
-};
+const buildModeledFieldGroupMember = metaFog => metaFog; // THIS MAY BREAK THINGS NOW WE'VE MOVED FUNCTION INTO CLOSURE BELOW
 
-const buildModeledField = metaFoG => {
-	const type = resolveType(metaFoG);
-	const className = buildFieldClassName(type);
-	if (!fmdModels[className]) {
-		throw new Error(`No metadata model "${className}" for type "${type}"`);
-	}
-	return new fmdModels[className](metaFoG);
-};
+const buildFieldSpecificMetaInClosure = (metaG, context) => {
 
-// Build Form Group Member
-const buildModeledFieldGroupMember = metaFoG => {
-	const modeledGroupMember = buildModeledField(metaFoG);
-	if (isContainer(metaFoG)) {
-		modeledGroupMember.meta = _buildFieldSpecificMeta(modeledGroupMember.meta);
-	} else if (isRepeatingContainer(metaFoG)) {
-		modeledGroupMember.meta = modeledGroupMember.meta.map(rcMem => ({ ...rcMem, meta: _buildFieldSpecificMeta(rcMem.meta) }));
-		modeledGroupMember.__defaultContainer = {
-			...modeledGroupMember.meta[0],
-			meta: _buildFieldSpecificMeta(modeledGroupMember.__defaultContainer),
-			name: '__defaultContainer',
-			button: ''
-		};
+	const resolveType = (metaFoG: StringMap<any>): string => {
+		if (metaFoG.type) {
+			return metaFoG.type;
+		}
+		if (isContainer(metaFoG)) {
+			return 'container';
+		}
+		if (isRepeatingContainer(metaFoG)) {
+			return 'repeatingContainer';
+		}
+		return 'text';
 	}
-	return modeledGroupMember;
-};
-
-// Build Form Group
- const buildModeledFieldGroupReducerIteree = (res, metaFoG) => ({ ...res, [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
-const _buildFieldSpecificMeta = metaG => isRepeatingContainer(metaG) ?
-	metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) : 
-	reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
-const buildFieldSpecificMeta = metaG => {
-	const withNames = addMissingNames(metaG);
-	return _buildFieldSpecificMeta(addMissingNames(metaG));
+	
+	const buildFieldClassName = (t: string): string => {
+		const start = t[0].toUpperCase() + t.slice(1);
+		if (start === 'Container' || start === 'RepeatingContainer' || start === 'Heading' || t.slice(-5) === 'Group') {
+			return start;
+		}
+		return start + 'Field';
+	};
+	
+	const buildModeledField = (metaFoG) => {
+		const type = resolveType(metaFoG);
+		const className = buildFieldClassName(type);
+		if (!fmdModels[className]) {
+			throw new Error(`No metadata model "${className}" for type "${type}"`);
+		}
+		return new fmdModels[className](metaFoG, context);
+	};
+	
+	// Build Form Group Member
+	const buildModeledFieldGroupMember = (metaFoG) => {
+		const modeledGroupMember = buildModeledField(metaFoG);
+		if (isContainer(metaFoG)) {
+			modeledGroupMember.meta = _buildFieldSpecificMeta(modeledGroupMember.meta);
+		} else if (isRepeatingContainer(metaFoG)) {
+			modeledGroupMember.meta = modeledGroupMember.meta.map(rcMem => ({ ...rcMem, meta: _buildFieldSpecificMeta(rcMem.meta) }));
+			modeledGroupMember.__defaultContainer = {
+				...modeledGroupMember.meta[0],
+				meta: _buildFieldSpecificMeta(modeledGroupMember.__defaultContainer),
+				name: '__defaultContainer',
+				button: ''
+			};
+		}
+		return modeledGroupMember;
+	};
+	
+	// Build Form Group
+	const buildModeledFieldGroupReducerIteree = (res, metaFoG) => ({ ...res, [metaFoG.name]: buildModeledFieldGroupMember(metaFoG) });
+	const _buildFieldSpecificMeta = metaG => isRepeatingContainer(metaG) ?
+		metaG.map(rcMem => _buildFieldSpecificMeta(rcMem)) : 
+		reduce(buildModeledFieldGroupReducerIteree, {}, metaG);
+	const buildFieldSpecificMeta = metaG => _buildFieldSpecificMeta(addMissingNames(metaG));
+
+	return buildFieldSpecificMeta(metaG);
 }
 
+
 // ---------------------------------------------------------------------------------------------------------------------
 // Generate mapping from source attributes
 // (used to grab data from model when using METAFIRST form generation)
@@ -229,7 +237,7 @@ const isAbsPath = path => typeof path === 'string' && path[0] === '/';
 const isRootPath = path => path === '/';
 const processPath = (parentPath, path) => isAbsPath(path) ? path : `${parentPath}.${path}`;
 
-const prependParentPathRecursive = (parentPath: string, obj: StringMap) => {
+const prependParentPathRecursive = (parentPath: string, obj: StringMap<any>) => {
 	return Object.entries(obj)
 		.map( ([key, mapping] ) => {
 			let mappingRes;
@@ -383,7 +391,8 @@ const buildFormGroupFunctionFactory = (fb: FormBuilder): (meta) => FormGroup =>
 		// Ensure that we have Field-Specific Metadata, not raw Objects
 		const metaWithNameKeys = addMissingNames(metaG); // <!--- DO WE REALLY HAVE TO CALL addMissingManes again here - it should have been done already?
 		// MAYBE only run this if first entry isn't right, for reasons of efficiency
-		const fieldModeledMeta = addMissingFieldSpecificMeta(metaWithNameKeys);
+		// const fieldModeledMeta = addMissingFieldSpecificMeta(metaWithNameKeys);
+		const fieldModeledMeta = metaWithNameKeys;
 		return _buildFormGroup(fieldModeledMeta);
 	};
 	return buildFormGroup;
@@ -422,7 +431,7 @@ const insertAfter = (obj, afterKey, key, val = null) => {
 };
 
 // Process reordeing instructions recursively
-const _execMetaReorderingInstructions = (metaG: StringMap) => {
+const _execMetaReorderingInstructions = (metaG: StringMap<any>) => {
 	let reorderedGroup = { ...metaG };
 	Object.entries(metaG).forEach(([key, metaFoG]) => {
 		if (metaFoG.before) {
@@ -437,7 +446,7 @@ const _execMetaReorderingInstructions = (metaG: StringMap) => {
 	return reorderedGroup;
 };
 
-const execMetaReorderingInstructions = (metaG: StringMap) => {
+const execMetaReorderingInstructions = (metaG: StringMap<any>) => {
 	// Repeating Containers (which have array meta *at this point*) can't be reordered, but other types of containers can
 	return Array.isArray(metaG) ? cloneDeep(metaG) : _execMetaReorderingInstructions(cloneDeep(metaG));
 };
@@ -454,6 +463,13 @@ const generateNewModel = (originalModel, updates) => {
 
 const updateObject = (obj, updates, createAdditionalKeys = false) => {
 	// THIS DOES NOT MUTATE obj, instead returning a new object
+	if (typeof obj !== 'object') {
+		obj = {};
+	}
+	console.log('obj is', obj, typeof obj);
+	if (Object.keys(obj).length === 0) {
+		createAdditionalKeys = true;
+	}
 	const shallowClone = { ...obj };
 	Object.entries(updates).forEach(([key, val]) => safeSet(shallowClone, key, val, createAdditionalKeys));
 	return shallowClone;
@@ -468,8 +484,9 @@ const safeSet = (obj, key, val, createAdditionalKeys = false) => {
 	if (val === currentVal) {
 		return;
 	}
-	if (nullOrScaler(currentVal)) {
-		console.log('safeSet nullOrScaler', key, val);
+
+	if (undefinedNullOrScalar(currentVal)) {
+		console.log('safeSet undefinedNullOrScalar', key, val);
 		obj[key] = val;
 	} else {
 		if (Array.isArray(currentVal)) {
@@ -492,8 +509,8 @@ const safeSet = (obj, key, val, createAdditionalKeys = false) => {
 	}
 };
 
-const nullOrScaler = val => {
-	if (val === null) { return true; }
+const undefinedNullOrScalar = val => {
+	if (val === null || val === undefined) { return true; }
 	const t = typeof val;
 	return t === 'number' || t === 'string' || t === 'boolean';
 };
@@ -557,6 +574,6 @@ const addMissingFieldSpecificMeta = metaG => Object.entries(metaG)
 
 export {
 	autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
-	buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory,
+	buildFieldSpecificMetaInClosure, extractFieldMappings, buildFormGroupFunctionFactory,
 	generateNewModel
 };

+ 36 - 13
src/app/dynaform/services/dynaform.service.ts

@@ -59,6 +59,12 @@
  * Typically you'd supply the component class instance, so that 'this' used in callbacks refers to the host component.
  *
  *
+ * DATA IN & DATA OUT
+ * ------------------
+ * updateForm - patch form values in FormGroup
+ * buildNewModel
+ *
+ *
  * LOWER-LEVEL METHODS
  * -------------------
  *
@@ -66,7 +72,7 @@
  * autoBuildModeledMeta(model, meta, createFromMeta) - takes a model and (lazy)metadata and returns expanded metadata
  *
  * buildFormGroup(metadata) - builds FormGroups from modelled metdata, recursively if necessary
- * buildFieldSpecificMeta(metadata) - use field metadta models to fill out metadata
+ * buildFieldSpecificMeta(metadata) - use field metadata models to fill out metadata
  * combineModelWithMeta(model, extraMeta) - automatically generated metadata for model then combines extra metadata
  * combineExtraMeta(metadata, extraMeta) - combine extra metadata into metatdata, lazyly and recursively
  * autoMeta(model) - generate basic metadata from a raw or mapped model, recursively if necessary
@@ -91,12 +97,12 @@ import { ModelMapperService } from './model-mapper.service';
 
 import {
 	autoMeta, combineModelWithMeta, combineExtraMeta, execMetaReorderingInstructions,
-	buildFieldSpecificMeta, extractFieldMappings, buildFormGroupFunctionFactory, generateNewModel
+	buildFieldSpecificMetaInClosure, extractFieldMappings, buildFormGroupFunctionFactory, generateNewModel
 } from './_formdata-utils';
 
 export interface IFormAndMeta {
 	form: FormGroup;
-	meta: StringMap;
+	meta: StringMap<any>;
 }
 
 export interface ICallbacks {
@@ -106,6 +112,10 @@ export interface ICallbacks {
 @Injectable()
 export class DynaformService {
 
+	public form: FormGroup;
+	public meta: StringMap<any>;
+	private context: any;
+
 	public buildFormGroup: (meta) => FormGroup;
 	private buildStrategy: 'MODELFIRST' | 'METAFIRST' = 'MODELFIRST'; // Make ENUM type
 	private callbacks: ICallbacks = {};
@@ -127,9 +137,16 @@ export class DynaformService {
 		}
 	}
 
-	build(model: StringMap, meta = {}, createFromMeta = false): IFormAndMeta {
-		// Short name for autoBuildFormGroupAndMeta
-		return this.autoBuildFormGroupAndMeta(model, meta, createFromMeta);
+	setContext(data: any): void {
+		// Set any runtime  data needed to build the form, e.g. options values
+		this.context = data;
+	}
+
+	build(model: StringMap<any>, meta = {}, createFromMeta = false): IFormAndMeta {
+		// Executes autoBuildFormGroupAndMeta and stores the result
+		const result = this.autoBuildFormGroupAndMeta(model, meta, createFromMeta);
+		({ form: this.form, meta: this.meta } = result);
+		return result;
 	}
 
 	register(callbacks: ICallbacks, cref: ComponentRef<any>['instance']) {
@@ -155,11 +172,17 @@ export class DynaformService {
 		}
 	}
 
-	buildNewModel(originalModel: StringMap, formVal: StringMap, meta: StringMap): StringMap {
+	updateForm(newModel: StringMap<any>, form?: FormGroup, meta?: StringMap<any>): void {
+		const mapping = extractFieldMappings(meta || this.meta); // Memoize
+		const mappedModel = this.modelMapper.reverseMap(newModel, mapping);
+		(form || this.form).patchValue(mappedModel);
+	}
+
+	buildNewModel(originalModel: StringMap<any>, formVal?: StringMap<any>, meta?: StringMap<any>): StringMap<any> {
 		console.log('%c *** buildNewModel *** ', this.conGreen);
-		const mapping = extractFieldMappings(meta); // Memoize
+		const mapping = extractFieldMappings(meta || this.meta); // Memoize
 		console.dir(mapping);
-		const updates = this.modelMapper.forwardMap(formVal, mapping);
+		const updates = this.modelMapper.forwardMap(formVal || this.form.value, mapping);
 		console.log('%c *** Updates *** ', this.conGreen);
 		console.dir(updates);
 		return generateNewModel(originalModel, updates);
@@ -168,7 +191,7 @@ export class DynaformService {
 	// -----------------------------------------------------------------------------------------------------------------
 	// Convenience methods combining several steps
 
-	autoBuildFormGroupAndMeta(model: StringMap, meta = {}, createFromMeta = false): IFormAndMeta {
+	autoBuildFormGroupAndMeta(model: StringMap<any>, meta = {}, createFromMeta = false): IFormAndMeta {
 		let _model;
 		if (this.buildStrategy === 'MODELFIRST') {
 			_model = model;
@@ -191,7 +214,7 @@ export class DynaformService {
 		};
 	}
 
-	autoBuildModeledMeta(model: StringMap, meta = {}, createFromMeta = false) {
+	autoBuildModeledMeta(model: StringMap<any>, meta = {}, createFromMeta = false) {
 		const modelWithMeta = this.combineModelWithMeta(model, meta, createFromMeta);
 		const reorderedMeta = execMetaReorderingInstructions(modelWithMeta);
 		return this.buildFieldSpecificMeta(reorderedMeta);
@@ -201,13 +224,13 @@ export class DynaformService {
 	// Build field-type-specific metadata using the form field models (see dynaform/models)
 
 	buildFieldSpecificMeta(meta) {
-		return buildFieldSpecificMeta(meta);
+		return buildFieldSpecificMetaInClosure(meta, this.context);
 	}
 
 	// -----------------------------------------------------------------------------------------------------------------
 	// Lower-level methods
 
-	combineModelWithMeta(model: StringMap, meta, createFromMeta = false) {
+	combineModelWithMeta(model: StringMap<any>, meta, createFromMeta = false) {
 		return combineModelWithMeta(model, meta, createFromMeta);
 	}
 

+ 32 - 32
src/app/dynaform/services/model-mapper.service.ts

@@ -105,12 +105,12 @@ const mapping = {
 */
 
 import { Injectable } from '@angular/core';
-import * as _ from 'lodash';
+import { get, set, unset, cloneDeep, merge, every, findLast, sortBy } from 'lodash';
 
 @Injectable()
 export class ModelMapperService {
 
-	mapping: StringMap;
+	mapping: StringMap<any>;
 	errors: string[] = [];
 
 	debug = false;
@@ -118,21 +118,21 @@ export class ModelMapperService {
 
 	constructor() { }
 
-	public setMapping(mapping: StringMap) {
+	public setMapping(mapping: StringMap<any>) {
 		this.mapping = mapping;
 	}
 
 	public forwardMap = (
-		model: StringMap,
-		mapping: StringMap = this.mapping,
+		model: StringMap<any>,
+		mapping: StringMap<any> = this.mapping,
 		mapMissing = false,
 		res = {},
 		stack = []
-	): StringMap => {
+	): StringMap<any> => {
 		// Map the input model onto res using the supplied mapping
 		Object.keys(model).forEach(key => {
 			const absPath = [...stack, key].join('.');
-			const _mapping = _.get(mapping, key, mapMissing ? key : false);
+			const _mapping = get(mapping, key, mapMissing ? key : false);
 			if (_mapping) {
 				const mappingType = this.resolveMappingType(_mapping);
 				switch(mappingType) {
@@ -155,7 +155,7 @@ export class ModelMapperService {
 								this.clog('Target Path', targetPath);
 								const func = storedContainerMapping.find(m => typeof m === 'function');
 								const funcRes = func(model[key]);
-								const fullRes = targetPath ? _.set({}, targetPath, funcRes) : funcRes; // Construct an update object from the model's root
+								const fullRes = targetPath ? set({}, targetPath, funcRes) : funcRes; // Construct an update object from the model's root
 								this.forwardMap(fullRes, {}, true, res);
 							} catch (e) {
 								this.clog(e);
@@ -184,23 +184,23 @@ export class ModelMapperService {
 	}
 
 	public lazyForwardMap = (
-		model: StringMap,
-		mapping: StringMap = this.mapping
-	): StringMap => this.forwardMap(model, mapping, true)
+		model: StringMap<any>,
+		mapping: StringMap<any> = this.mapping
+	): StringMap<any> => this.forwardMap(model, mapping, true)
 
 	public reverseMap = (
-		model: StringMap,
-		mapping: StringMap = this.mapping,
+		model: StringMap<any>,
+		mapping: StringMap<any> = this.mapping,
 		mapMissing = false,
 		pathsToDelete = [],
 		stack = []
-	): StringMap => {
+	): StringMap<any> => {
 		// pathToDelete contains a list of source paths to delete from the model, leaving the missing to be straight-mapped
 		if (!mapping) {
 			throw new Error('Attempting to use Model Mapper without mapping');
 		}
 		const res = {};
-		const modelClone = stack.length ? model : _.cloneDeep(model); // Clone the model unless inside a recursive call
+		const modelClone = stack.length ? model : cloneDeep(model); // Clone the model unless inside a recursive call
 		Object.keys(mapping).filter(key => key !== '__').forEach(key => {
 			const dataMapping = mapping[key];
 			const mappingType = this.resolveMappingType(dataMapping);
@@ -208,7 +208,7 @@ export class ModelMapperService {
 				case 'simple':
 					{
 						// A simple path
-						const value = _.get(modelClone, dataMapping);
+						const value = get(modelClone, dataMapping);
 						if (typeof value !== 'undefined') {
 							this.deepSet(res, key, value);
 						}
@@ -251,9 +251,9 @@ export class ModelMapperService {
 			const deepestPathsLast = this.sortByPathDepth(pathsToDelete);
 			while(deepestPathsLast.length) {
 				const path = deepestPathsLast.pop();
-				const t = typeof _.get(modelClone, path);
+				const t = typeof get(modelClone, path);
 				if (t === 'number' || t === 'string' || t === 'boolean') {
-					_.unset(modelClone, path);
+					unset(modelClone, path);
 				}
 			}
 			const modelRemainder = this.deepCleanse(modelClone);
@@ -265,9 +265,9 @@ export class ModelMapperService {
 	}
 
 	public lazyReverseMap = (
-		model: StringMap,
-		mapping: StringMap = this.mapping
-	): StringMap => this.reverseMap(model, mapping, true)
+		model: StringMap<any>,
+		mapping: StringMap<any> = this.mapping
+	): StringMap<any> => this.reverseMap(model, mapping, true)
 
 	public getErrors() {
 		return this.errors;
@@ -285,12 +285,12 @@ export class ModelMapperService {
 			if (
 				Array.isArray(mappingPath)
 				&& mappingPath.length === 2
-				&& _.every(mappingPath, m => typeof m === 'function')
+				&& every(mappingPath, m => typeof m === 'function')
 				||
 				Array.isArray(mappingPath)
 				&& mappingPath.length === 3
 				&& (typeof mappingPath[0] === 'number' || typeof mappingPath[0] === 'string')
-				&& _.every(mappingPath.slice(1), m => typeof m === 'function')
+				&& every(mappingPath.slice(1), m => typeof m === 'function')
 			) {
 				mappingType = 'functional';
 			} else {
@@ -309,9 +309,9 @@ export class ModelMapperService {
 		if (path === '/') {
 			arg = model; // '/' indicates use the entire model
 		} else {
-			arg = _.get(model, path.replace(/^\//, ''));
+			arg = get(model, path.replace(/^\//, ''));
 		}
-		const func = _.findLast(fnMapping, m => typeof m === 'function');
+		const func = findLast(fnMapping, m => typeof m === 'function');
 		try {
 			result = func(arg);
 		} catch(e) {
@@ -324,25 +324,25 @@ export class ModelMapperService {
 		// NOTE: This mutates the incoming object at the moment, so doesn't reed to return a value
 		// Maybe rewrite with a more functional approach?
 		// Will deep merge where possible
-		const currentVal = _.get(obj, mappingPath);
+		const currentVal = get(obj, mappingPath);
 		const t = typeof currentVal;
 		if (t === 'undefined') {
-			_.set(obj, mappingPath, valueToSet);
+			set(obj, mappingPath, valueToSet);
 		} else if (t === 'number' || t === 'string' || t === 'boolean') {
 			// We can only overwrite existing scalar values, not deep merge
 			if (overwrite) {
 				this.errors.push('WARNING: Overwriting scalar value at', mappingPath);
-				_.set(obj, mappingPath, valueToSet);
+				set(obj, mappingPath, valueToSet);
 			} else {
 				this.errors.push('WARNING: Discarding scalar value at', mappingPath, 'as exisiting non-scalar value would be overwritten');
 			}
 		} else if (t === 'object' && typeof valueToSet === 'object') {
 			// Deep merge
-			let merged = _.merge(currentVal, valueToSet);
+			let merged = merge(currentVal, valueToSet);
 			if (!overwrite) {
-				merged = _.merge(merged, currentVal); // Is there a better way?
+				merged = merge(merged, currentVal); // Is there a better way?
 			}
-			_.set(obj, mappingPath, merged);
+			set(obj, mappingPath, merged);
 		} else {
 			this.errors.push('WARNING: Could not merge', typeof valueToSet, 'with object');
 		}
@@ -367,7 +367,7 @@ export class ModelMapperService {
 		return cleansedObj;
 	}
 
-	private sortByPathDepth = pathArr => _.sortBy(pathArr, p => p.split('.').length);
+	private sortByPathDepth = pathArr => sortBy(pathArr, p => p.split('.').length);
 
 	private clearErrors() {
 		this.errors = [];

+ 1 - 1
src/app/dynaform/utils.ts

@@ -46,7 +46,7 @@ const arrayToMeta = array => array.map(val => ({ name: val, 'value' : val }));
 
 // Exclude 'fieldsToExclude' from obj, returning a new object
 // fieldsToExclude can be an array of field keys or a CSV of field keys
-const excludeFields = (obj: StringMap, fieldsToExclude: string | string[]) => {
+const excludeFields = (obj: StringMap<any>, fieldsToExclude: string | string[]) => {
 	const ex = Array.isArray(fieldsToExclude) ? fieldsToExclude : fieldsToExclude.split(',').map(key => key.trim());
 	return Object.entries(obj).reduce(
 		(res, [key, val]) => ex.includes(key) ? res : { ...res, [key]: val },

+ 0 - 13
src/app/ng-dynaform.code-workspace

@@ -1,13 +0,0 @@
-{
-	"folders": [
-		{
-			"path": "/Users/rk/play/ng-dynaform"
-		},
-		{
-			"path": "."
-		},
-		{
-			"path": "dynaform"
-		}
-	]
-}

+ 6 - 0
src/styles.scss

@@ -127,3 +127,9 @@ div.col-sm-8 {
 	}
 }
 
+// ---------------------------------------------------------------------------------------------------------------------
+// Debugging
+
+.__debug {
+	margin: 2em 0;
+}

+ 1 - 1
src/typings.d.ts

@@ -8,4 +8,4 @@ declare module "*.json" {
     export default value;
 }
 
-interface StringMap { [s: string]: any; }
+interface StringMap<T> { [s: string]: T; }