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

Generate typed FormGroup including validations from interface/model through declarative info of validations #52559

Open
zijianhuang opened this issue Nov 7, 2023 · 3 comments
Labels
area: compiler Issues related to `ngc`, Angular's template compiler area: forms
Milestone

Comments

@zijianhuang
Copy link

zijianhuang commented Nov 7, 2023

Which @angular/* package(s) are relevant/related to the feature request?

compiler-cli, compiler

Description

"As of Angular 14, reactive forms are strictly typed by default.", however, constructing typed FormGroup in TypeScript codes is still almost always a manual process.

It can bring productivity to application programming that Angular compiler can generate a typed FormGroup from declarative info of validations.

And such design concept is illustrated in WebApiCoreNG2FormGroupClientAuto.ts generated by WebApiClientGen according to declarative info of data constraints in C#.

Declarative info of data constraints in C#

	/// <summary>
	/// Base class of company and person
	/// </summary>
	[DataContract(Namespace = Constants.DataNamespace)]
	public class Entity
	{
		public Entity()
		{
			Addresses = new List<Address>();
		}

		[DataMember]
		public Guid? Id { get; set; }

		/// <summary>
		/// Name of the entity.
		/// </summary>
		[DataMember(IsRequired =true)]//MVC and Web API does not care
		[System.ComponentModel.DataAnnotations.Required]//MVC and Web API care about only this
		[MinLength(2), MaxLength(255)]
		public string Name { get; set; }

		/// <summary>
		/// Multiple addresses
		/// </summary>
		[DataMember]
		public IList<Address> Addresses { get; set; }


		[DataMember]
		public virtual ObservableCollection<PhoneNumber> PhoneNumbers { get; set; }

		public override string ToString()
		{
			return Name;
		}

		[DataMember]
		public Uri Web { get; set; }

		[DataMember, EmailAddress, MaxLength(255)]
		public string EmailAddress { get; set; }
	}

	[DataContract(Namespace = Constants.DataNamespace)]
	public class Person : Entity
	{
		[DataMember]
		public string Surname { get; set; }
		[DataMember]
		public string GivenName { get; set; }

		/// <summary>
		/// Date of Birth.
		/// This is optional.
		/// </summary>
		[DataMember]
		public DateOnly? DOB { get; set; }

		[DataMember]
		[DataType(DataType.Date)]
		public DateTimeOffset? Baptised { get; set; }

		public override string ToString()
		{
			return Surname + ", " + GivenName;
		}

	}

Typed FormGroups generated

	export interface Person extends DemoWebApi_DemoData_Base_Client.Entity {

		/** Data type: Date */
		baptised?: Date | null;

		/**
		 * Date of Birth.
		 * This is optional.
		 */
		dob?: Date | null;
		givenName?: string | null;
		surname?: string | null;
	}
	export interface PersonFormProperties extends DemoWebApi_DemoData_Base_Client.EntityFormProperties {

		/** Data type: Date */
		baptised: FormControl<Date | null | undefined>,

		/**
		 * Date of Birth.
		 * This is optional.
		 */
		dob: FormControl<Date | null | undefined>,
		givenName: FormControl<string | null | undefined>,
		surname: FormControl<string | null | undefined>,
	}
	export function CreatePersonFormGroup() {
		return new FormGroup<PersonFormProperties>({
			emailAddress: new FormControl<string | null | undefined>(undefined, [Validators.email, Validators.maxLength(255)]),
			id: new FormControl<string | null | undefined>(undefined),
			name: new FormControl<string | null | undefined>(undefined, [Validators.required, Validators.minLength(2), Validators.maxLength(255)]),
			web: new FormControl<string | null | undefined>(undefined),
			baptised: new FormControl<Date | null | undefined>(undefined),
			dob: new FormControl<Date | null | undefined>(undefined),
			givenName: new FormControl<string | null | undefined>(undefined),
			surname: new FormControl<string | null | undefined>(undefined),
		});

	}

There are some limitations in such code generation for type FormGrooup:

  1. Generic types not supported, because FormGroup takes only concrete types.
  2. (Nested) member fields of complex types not supported because of various reasons.

Proposed solution

Directive in Angular is similar to attribute in C#. Attributes in C# is used by .NET run time, and directive is used by Angular compiler. I would purpose such solution with some pseudo directives.

	export interface Entity {

		/**
		 * Multiple addresses
		 */
		addresses?: Array<DemoWebApi_DemoData_Client.Address> | null;

		/** Max length: 255 */
		@validation([Validators.email, Validators.maxLength(255)])
		emailAddress?: string | null;
		id?: string | null;

		/**
		 * Name of the entity.
		 * Required
		 * Min length: 2
		 * Max length: 255
		 */
		@validation('Super Hero', [Validators.required, Validators.minLength(2), Validators.maxLength(255)])
		name: string;
		phoneNumbers?: Array<DemoWebApi_DemoData_Client.PhoneNumber> | null;
		web?: string | null;
	}

Application codes:

const myFormGroup = FormGroup.create<Entity>();

NG compiler or its pre-processor or IDE generates

const myFormGroup = new FormGroup({
			emailAddress: new FormControl<string | null | undefined>(undefined, [Validators.email, Validators.maxLength(255)]),
			id: new FormControl<string | null | undefined>(undefined),
			name: new FormControl<string | null | undefined>('Super Hero', [Validators.required, Validators.minLength(2), Validators.maxLength(255)]),
			web: new FormControl<string | null | undefined>(undefined),
		});

Sometimes a nested complex object like "addresses" is to be included in the FormGroup, then a directive like @formGroup(1) or @formArray() could decorate a respective member field.

The reason why @formGroup() has a number parameter is to avoid endless recursive nesting.

Alternatives considered

If such feature is implemented, WebApiClientGen just need to exporting respective directives according to C# attributes.

An alternative design is to have a declarative object to associated a data model/interface with a validation map, mapping some member fields with some validations. However, such approach may look almost the same as the current way, and require a lot more design change in Angular compiler. And this is not friendly to code generators like WebApiClientGen

Remarks:
Comparing with #46864, this proposal emphasizes on validations through declarative attributes (.NET) and directives (Angular), and cover more scenarios of application programming.

@zijianhuang
Copy link
Author

zijianhuang commented Nov 8, 2023

Extending from the OP, the some validators may have an extra parameter to tell the compiler whether to inject some HTML attributes to , for example, maxlength, type email etc.

@validation([Validators.email(true), Validators.maxLength(255, true), Validators.required(true)])
		emailAddress?: string | null;

NG HTML template:

<input formControlName="emailAddress">

The compiled code will create HTML:

<input formControlName="emailAddress" maxlength="128" type="email" required>

So the app will have both UI constraints and code level validations.

Remarks

Since a typed FormGroup generally is created in the constructor, and is assigned to a read only member field of the component which can be assigned only inside the constructor, the compiler should have enough info to process.

@zijianhuang
Copy link
Author

Noted that the code gen skips the properties of complex types and array type. However, it is easy to compose a FormGroup with nested Form Groups and nested Form Arrays. For example:

export interface HeroWithNestedFormProperties extends DemoWebApi_Controllers_Client.HeroFormProperties {
    address?: FormGroup<DemoWebApi_DemoData_Client.AddressFormProperties>,
    phoneNumbers?: FormArray<FormGroup<DemoWebApi_DemoData_Client.PhoneNumberFormProperties>>,
}

export function CreateHeroWithNestedFormGroup() {
    const fg: FormGroup<HeroWithNestedFormProperties> = DemoWebApi_Controllers_Client.CreateHeroFormGroup();
    fg.controls.address = DemoWebApi_DemoData_Client.CreateAddressFormGroup();
    fg.controls.phoneNumbers = new FormArray<FormGroup<DemoWebApi_DemoData_Client.PhoneNumberFormProperties>>([]);
    return fg;
}

For more details, please check hero-detail.component.ts

Therefore, the requested feature may take similar approach: skip complex property and array property, and let application programmers decide whether to include them in the nested Form Groups and Form Arrays in a Form Group.

@zijianhuang
Copy link
Author

Since generated typed forms from Swagger / OpenAPI definitions and ASP.NET Web API had been implemented, it shall be nicer that the Angular team could accelerate the development of generating type forms for client only data.

More examples of generated typed forms: https://github.com/zijianhuang/openapiclientgen/tree/master/Tests/NG2Tests/Results

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: compiler Issues related to `ngc`, Angular's template compiler area: forms
Projects
None yet
Development

No branches or pull requests

2 participants