123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- /* *********************************************************************************************************************
- * Dynaform Service
- * *********************************************************************************************************************
- *
- * BUILD
- * -----
- * This is the main method you'll use to build forms from models and metadata:
- *
- * build(model, meta = {}, createFromMeta = false)
- *
- * Takes a model and (lazy)metadata and returns an object containing a FormGroup and Modeled MetaData :
- * {
- * form: FormGroup,
- * meta: ModeledMetaData
- * }
- *
- * meta is optional, and if not supplied all the model fields will become Text inputs in the FormGroup,
- * with their labels set to the un-camel-cased property name
- *
- * createFromMeta is optional.
- * If true it will create new fields in the FormGroup even when they don't already exist in the model,
- * but exist in the metadata. This is NOT the default behaviour, except when an empty model is supplied.
- * i.e. It defaults to false, except when model == {}
- *
- * Usage
- * -----
- *
- * build(model) - build everything from the model
- * build({}, meta) - build everything from the metadata
- * build(model, meta) - build by combining the model with metadata, lazyily (not every model field needs metadata, as sensible defaults)
- * build(model, meta, true) - build by combining model with metadata, creating new fields from metadata points that don't occur in the model
- *
- *
- * BUILD STRATEGYS
- * ---------------
- *
- * MODELFIRST - The model determinies the shape of the form
- * METAFIRST - The metadata determines the shape of the form
- *
- * Set the build strategy using the setBuildStrategy method e.g.
- *
- * setBuildStrategy('MODELFIRST') - the default
- * setBuildStrategy('NETAFIRST')
- *
- * then use the build function in the normal way
- *
- *
- * REGISTER
- * --------
- * Registers callbacks attached to the form (e.g. to buttons), identified by strings.
- *
- * register({
- * 'SAYHELLO': () => { alert('HELLO') },
- * 'SEARCH': execSearch,
- * 'NEW': addNew,
- * }, varToBind);
- *
- * If varToBind is supplied it is bound as 'this' to the functions.
- * 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
- *
- *
- * 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
- * -------------------
- *
- * autoBuildFormGroupAndMeta(model, meta, createFromMeta) - synonym for build
- * 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 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
- *
- *
- * NOTES
- * -----
- * This class acts as an injectable wraper around the exports of _formdata-utils.ts,
- * as well as creating a buildFormGroup function using the injected FormBuilder singleton
- *
- *
- * EXAMPLES
- * --------
- *
- * TO ADD ...
- *
- */
- 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,
- resetAsyncValidatorsRecursive,
- touchAndUpdateValidityRecursive, resetValidityRecursive, advanceUpdateStategyOfInvalidControlsRecursive,
- generateNewModel, updateMeta
- } from './_formdata-utils';
- import { Option } from './../models/field.model';
- export interface IFormAndMeta {
- form: FormGroup;
- meta: StringMap<any>;
- }
- export interface ICallbacks {
- [index: string]: () => void;
- }
- @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 = {};
- private conGreen = 'color: white; background-color: green; font-weight: bold;';
- constructor(private fb: FormBuilder, private modelMapper: ModelMapperService) {
- this.buildFormGroup = buildFormGroupFunctionFactory(fb);
- }
- setBuildStrategy(str): void {
- switch (str.toUpperCase()) {
- case 'MODELFIRST': // Build from model, combining optional extra metadata
- case 'METAFIRST': // Build from metadata, sourcing values from the model (optionally, using meta's 'source' attribute)
- this.buildStrategy = str.toUpperCase();
- break;
- default:
- throw new Error(`DYNAFORM: Unknown Build Strategy ${str} - should be MODELFIRST or METAFIRST`);
- }
- }
- 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']) {
- if (cref) {
- // Bind the component instance to the callback, so that 'this' has the context of the component
- Object.entries(callbacks).forEach(([key, fn]) => this.callbacks[key] = fn.bind(cref));
- } else {
- Object.assign(this.callbacks, callbacks);
- }
- }
- call(fnId: string) {
- // Handle callbacks
- if (typeof this.callbacks[fnId] === 'function') {
- try {
- this.callbacks[fnId]();
- } catch (e) {
- console.error('Error thrown during callback', fnId);
- console.error(e);
- }
- } else {
- console.error('Dynaform has no registered callback for', fnId);
- }
- }
- 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);
- }
- 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;
- }
- resetForm(form?: FormGroup): void {
- // Loop through the form and call reset on any Async validators
- resetAsyncValidatorsRecursive(form || this.form);
- (form || this.form).reset();
- }
- 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
- console.dir(mapping);
- const updates = this.modelMapper.forwardMap(formVal || this.form.value, mapping);
- console.log('%c *** Updates *** ', this.conGreen);
- console.dir(updates);
- 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
- autoBuildFormGroupAndMeta(model: StringMap<any>, meta = {}, createFromMeta = false): IFormAndMeta {
- let _model;
- if (this.buildStrategy === 'MODELFIRST') {
- _model = model;
- } else if (this.buildStrategy === 'METAFIRST') {
- // Generate mapping from the 'source' attributes in the metadata (compatible with the model-mapper service)
- // Then grab values from the model
- const mapping = extractFieldMappings(meta);
- _model = this.modelMapper.reverseMap(model, mapping, false); // <-- false - excludes bits of model not explicitly referenced in meta
- console.log('%c *** Mapping and Mapped Model *** ', this.conGreen);
- console.dir(mapping);
- console.dir(_model);
- }
- if (this.buildStrategy === 'METAFIRST' || Object.keys(model).length === 0) {
- createFromMeta = true;
- }
- const fullMeta = this.autoBuildModeledMeta(_model, meta, createFromMeta);
- return {
- form: this.buildFormGroup(fullMeta),
- meta: fullMeta
- };
- }
- autoBuildModeledMeta(model: StringMap<any>, meta = {}, createFromMeta = false) {
- const modelWithMeta = this.combineModelWithMeta(model, meta, createFromMeta);
- const reorderedMeta = execMetaReorderingInstructions(modelWithMeta);
- return this.buildFieldSpecificMeta(reorderedMeta);
- }
- // -----------------------------------------------------------------------------------------------------------------
- // Build field-type-specific metadata using the form field models (see dynaform/models)
- buildFieldSpecificMeta(meta) {
- return buildFieldSpecificMetaInClosure(meta, this.context);
- }
- // -----------------------------------------------------------------------------------------------------------------
- // Lower-level methods
- combineModelWithMeta(model: StringMap<any>, meta, createFromMeta = false) {
- return combineModelWithMeta(model, meta, createFromMeta);
- }
- combineExtraMeta(meta, extraMeta, createFromExtra = false) {
- return combineExtraMeta(meta, extraMeta, createFromExtra);
- }
- autoMeta(model) {
- 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');
- }
- }
- }
|