Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a FormlyFieldConfig builder that supports type safe creation of configs #2571

Open
StephenCooper opened this issue Oct 28, 2020 · 10 comments

Comments

@StephenCooper
Copy link

StephenCooper commented Oct 28, 2020

Type safe config

I am working in Typescript with Formly and after setting up many forms I have created custom FormlyFieldConfig builder methods for the common input types. For, example an input, a number input, a date picker and so on.

As we often have a Typescript interface or class representing our form model I looked to see if it was possible to achieve type safety with my config builders. I found that this is possible and it makes for a fantastic developer experience. Retrofitting this type safety highlighted some typos in key values during development.

I have a live example of the type safe FormlyFieldConfig builder on stackblitz here.

https://stackblitz.com/edit/type-safe-formly-builder-interface?file=src/app/formly-builder.ts

Typing key property of FormlyFieldConfig

The code in formly-builder is what I am proposing to be added into formly to help others easily take advantage of type safe config builders.

The core part is the following type definitions.

/** Ensure that the key is a valid property of the model Model and that the type of the value for that property is FieldType */
export type FormlyFieldKeyOfType<Model, FieldType> = {
  [Key in keyof Model]: Model[Key] extends FieldType ? Key & string : never
}[keyof Model];

/** Extension of FormlyFieldConfig with a restricted key type that is a string but also is a valid key of Model and Model[key] is of type FieldType */
export interface FormlyFieldConfigKeyed<Model, FieldType>
  extends FormlyFieldConfig {
  key: FormlyFieldKeyOfType<Model, FieldType>;
}

Creating a FormlyConfigBuilder class

Using these we can then define the FormlyConfigBuilder class for a given Model. This can have a build method with two potential overloads. The first takes a FieldType parameter which then enforces that the type checking for the key against the given model. We could also provide an overload that does not provide a FieldType but this would only check that the key is a property of the Model but would not check the type of that property.

/** Formly type safe config builder for given Model interface.
 */
export class FormlyConfigBuilder<Model> {

  /** fieldConfig must have key property that is a valid keyOf Model and its type equals FieldType */
  public build<FieldType>(
    fieldConfig: FormlyFieldConfigKeyed<Model, FieldType>
  ): FormlyFieldConfig;

  /** fieldConfig must have key property that is a valid keyOf Model */
  public build(
    fieldConfig: FormlyFieldConfigKeyed<Model, any>
  ): FormlyFieldConfig {
    return fieldConfig;
  }
}

Alternative standalone builder function

/** Function to build config which applies keyOf and value type check to config based of the Model and FieldType */
export function buildConfig<Model, FieldType>(
  fieldConfig: FormlyFieldConfigKeyed<Model, FieldType>
): FormlyFieldConfig {
  return fieldConfig;
}

Example use of FormlyConfigBuilder

Given the following interface we could then use our form builder as follows.

export interface FormModel {
  name: string;
  age: number;
  dob: Date;
}

Using this builder we get smart auto complete of only the valid property from the FormModel when we set the type.

fb = new FormlyConfigBuilder<FormModel>();
fields: FormlyFieldConfig[] = [
    this.fb.build<string>({ key: "name" }), 
    this.fb.build<number>({ key: "age" }), 
    this.fb.build<Date>({ key: "dob" }),
];

Expected use via extending FormlyConfigBuilder

With the FormlyConfigBuilder in place it is very easy to then create your own custom config builder as an extension of it. This can then be used to hide the explicit typing.

export class AppFormlyConfigBuilder<T> extends FormlyConfigBuilder<T> {

  number(
    fieldConfig: FormlyFieldConfigKeyed<T, number>
  ): FormlyFieldConfig {
    return this.build<number>({
      type: "input",
      ...fieldConfig,
      templateOptions: { type: "number", ...fieldConfig.templateOptions },
    });
  }
}

This can then be used as follows

fb = new AppFormlyConfigBuilder<FormModel>();
fields: FormlyFieldConfig[] = [
    this.fb.number({ key: "age" }), 
];

Benefits

Type checking of key values to avoid typos and copy and paste errors. Enables auto complete while setting up the form in IDEs which is a huge time saver.

The base class and the type give developers a basis to build their own custom config builders in a type safe manner.

Current limitations

Does not currently support dot chained key values such as address.house for example.

Happy to create a PR for this feature if you think it is worth while.

@StephenCooper
Copy link
Author

StephenCooper commented Oct 28, 2020

Similar concept requested in #1850 but issue was self closed after author worked out how to enforce keyOf for a given model. Suggests there may be other devs trying to create their own types for FormlyFieldConfigs already.

@StephenCooper
Copy link
Author

Thanks, @aitboudad, for the label. Do you have any further thoughts on this issue or know anyone who would like to weigh in? Otherwise should I start work on the PR?

@aitboudad
Copy link
Member

Hi @StephenCooper, I'm aware of that issue and it'll be part of v6 for sure. We're going to work on it together, I just need to find some spare time to do some research on that area and I'll let you know the next step :)

@nthonymiller
Copy link

nthonymiller commented Jan 30, 2021

I put together an example of how type safe formly builder might work: Formly Builder
Would appreciate feedback on the API.

@aitboudad
Copy link
Member

aitboudad commented May 18, 2021

I think its time to give this issue what it deserves :), the first step I think is to improve our JSON format:

Step 1

1- templateOptions => ui (or props 🤔):

{
  key: 'name',
  type: 'input',
  ui: { label: 'Name' },
  expressionProperties: {
    'ui.disabled': 'model.disabled'
  }
}

2- expressionProperties|hideExpression => expressions:

{
  key: 'name',
  type: 'input',
  expressions: {
    hide: 'model.disabled',
  }
}

3- fieldGroup => group:

{
  key: 'address',
  group: [ ... ]
}

4- fieldArray => array:

{
  key: 'addresses',
  array: { ... }
}

let me know if there is more parts to improve!

Step 2

define helpers for each type, that supports type-safe.

Step 3

provide a builder as a sub-package

@kenisteward
Copy link
Collaborator

kenisteward commented May 18, 2021 via email

@nthonymiller
Copy link

@aitboudad I agree with improving the JSON format as sometimes I get confused as the format is trying to support Field, Group and Array formats.

This is part of the reason why I wrote a builder that I've been using in my own application to help with this.

Example can be found here: Formly Builder Example

Would be happy to help on this.

@StephenCooper
Copy link
Author

Probably similar to what @kenisteward described I have been linking the type to a extended FormlyTemplateOptions to enable the developer to know what template options they can specify.

For example a DatePicker control supports minDate and maxDate.

export interface FormlyDateTemplateOptions extends FormlyTemplateOptions {
  minDate?: Date;
  maxDate?: Date;
}

So being able to provide a custom type to represent the templateOptions / ui properties would be great.

@aitboudad
Copy link
Member

Moved the Step 1 to #2853

@kenisteward Sure

@StephenCooper we'll reach that in Step 2, I've done a similar approach recently I'll share my work once done with step1 :)

@nthonymiller the builder you've created is close to what I've in mind, I'll let you know once reached that part.

@rickynguyen4590
Copy link

rickynguyen4590 commented Apr 15, 2022

I am creating Type safe builder also, my idea is similar with @StephenCooper. But the builder is not Injectable so I can't use some internal providers. What I can do it create factory to create a builder like below.

  formGroup = new FormGroup({});

  fb = factory.createBuilder<IClaimSubClaimModel>();

  fields: NvFormFieldConfig[] = [
    this.fb.field('ClaimDetail', {
      type: 'nv-dropdown',
      templateOptions: {
        options: this.subClaimStore.select(s => s.detailCasualties)
      }
    })
  ];

Do we have any ideas to make the builder support Injectable and type safe together without factory?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

5 participants