Skip to content

Angular Reactive Forms

fonlow edited this page Feb 1, 2024 · 10 revisions

"As of Angular 14-16, reactive forms are strictly typed by default.", however, constructing typed FormGroup in TypeScript codes is still a manual process. Before the Angular team would develop respective features, WebApiClientGen can generate client API codes that provide typed FormGroups including validation codes according to annotation attributes decorating class properties or fields.

Article "Generate Typed FormGroup of Angular Reactive Forms with ASP.NET Core Web API" provides more details about application programming with the generated codes.

Examples

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),
		});

	}

Mappings

.NET Validation Attribute to Angular Validator

Attribute NG Validator
Required required
MaxLength maxLength
MinLength minLength
StringLength minLength maxLength if each is greater than zero
Range min max if Maximum or Minimum is defined
EmailAddress email
RegularExpression pattern

.NET Integral Numeric Type to Angular Validator

Type NG Validator
sbyte Validators.min(-127), Validators.max(127)
byte Validators.min(0), Validators.max(256)
short Validators.min(-32768), Validators.max(32767)
ushort Validators.min(0), Validators.max(65535)
int Validators.min(-2147483648), Validators.max(2147483647)
uint Validators.min(0), Validators.max(4294967295)

Remarks:

  • For .NET long and ulong, the situation could be tricky, as in JavaScript, Number.MAX_SAFE_INTEGER=9007199254740991. It is better to let application programmers decide what to do.
  • If a member property is of one of these integral types and is decorated by "RangeAttribute", the code gen won't yield validators based on the integral type but "RangeAttribute".

POCO on Web API:

	[DataContract(Namespace = Constants.DataNamespace)]
	public class IntegralEntity : Entity
	{
		[DataMember]
		public sbyte SByte { get; set; }

		[DataMember]
		public byte Byte { get; set; }

		[DataMember]
		public short Short { get; set; }

		[DataMember]
		public ushort UShort { get; set; }

		[DataMember]
		public int Int { get; set; }

		[DataMember]
		public uint UInt { get; set; }

		[Range(-1000, 1000000)]
		[DataMember]
		public int ItemCount { get; set; }
	}

Typed Reactive Forms:

	export interface IntegralEntity extends DemoWebApi_DemoData_Base_Client.Entity {
		byte?: number | null;
		int?: number | null;

		/** Range: inclusive between -1000 and 1000000 */
		itemCount?: number | null;
		sByte?: number | null;
		short?: number | null;
		uInt?: number | null;
		uShort?: number | null;
	}
	export interface IntegralEntityFormProperties extends DemoWebApi_DemoData_Base_Client.EntityFormProperties {
		byte: FormControl<number | null | undefined>,
		int: FormControl<number | null | undefined>,

		/** Range: inclusive between -1000 and 1000000 */
		itemCount: FormControl<number | null | undefined>,
		sByte: FormControl<number | null | undefined>,
		short: FormControl<number | null | undefined>,
		uInt: FormControl<number | null | undefined>,
		uShort: FormControl<number | null | undefined>,
	}
	export function CreateIntegralEntityFormGroup() {
		return new FormGroup<IntegralEntityFormProperties>({
			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, [Validators.pattern('https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*)')]),
			byte: new FormControl<number | null | undefined>(undefined, [Validators.min(0), Validators.max(256)]),
			int: new FormControl<number | null | undefined>(undefined, [Validators.min(-2147483648), Validators.max(2147483647)]),
			itemCount: new FormControl<number | null | undefined>(undefined, [Validators.min(-1000), Validators.max(1000000)]),
			sByte: new FormControl<number | null | undefined>(undefined, [Validators.min(-127), Validators.max(127)]),
			short: new FormControl<number | null | undefined>(undefined, [Validators.min(-32768), Validators.max(32767)]),
			uInt: new FormControl<number | null | undefined>(undefined, [Validators.min(0), Validators.max(4294967295)]),
			uShort: new FormControl<number | null | undefined>(undefined, [Validators.min(0), Validators.max(65535)]),
		});

	}

Intentional Limitations

Property or field of nested complex objects is not supported.

Property of field of array is not supported.

Default value of property is not supported, and System.ComponentModel.DefaultValueAttribute is not supported.

If you want a FormGroup with nested FormGroups and FormArrays, it is fairly easy to implement at the application codes level, 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;
}

More Examples of Generated Codes

Clone this wiki locally