Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jessica Eisner
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Jessica Eisner

Quick Reference For NgModel Values And Template-Driven Forms In Angular 7.2.13

By
Published in Comments (14)

NOTE: This post is primarily a note-to-self for future reference.

As I've demonstrated recently, template-driven forms in Angular 7.2.13 can be very dynamic. In fact, you can even listen to the reactive events using a template-driven forms model. However, in the vast majority of use-cases, I just need a good-old Text Input with a simple [(ngModel)] binding. As such, some of the details around non-text-input consumption are not always front-of-mind for me. This is why yesterday, when Charles Robertson asked me why his Radio Control wasn't working as expected, I didn't have the answer at my fingertips. In order to prevent such fumbling in the future, I wanted to create a quick reference for the basic NgModel value bindings that can be used in template-driven forms in Angular 7.2.13.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

With template-driven forms in Angular, all of the form controls are linked to the view-model using NgModel input bindings. However, with some of the controls, we have to tell Angular how the NgModel relates to other parts of the view-model (such as with Selects and Radio Buttons). In such cases, we need to use either the [value] or [ngValue] input - depending on the control - in order to create proper view-model synchronization.

To see this in action, I've created a template-driven form that uses text-inputs, radio buttons, checkboxes, single selects, and multi-selects; and then, writes the current view-model to the browser using the JSON pipe. To set the context for this experiment, let's first look at the component code:

// Import the core angular services.
import { Component } from "@angular/core";
import { NgForm } from "@angular/forms";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

interface Genre {
	id: string;
	name: string;
	adultsOnly: boolean;
}

interface Movie {
	id: string;
	name: string;
	releasedAt: string;
}

interface Snack {
	id: string;
	name: string;
}

interface WatchOption {
	id: string;
	label: string;
}

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.less" ],
	templateUrl: "./app.component.htm"
})
export class AppComponent {

	public form: {
		favoriteGenres: {
			action: boolean;
			commedy: boolean;
			documentary: boolean;
			drama: boolean;
			horror: boolean;
			scifi: boolean;
		},
		favoriteMovie: Movie | null,
		favoriteSnacks: Snack[],
		user: {
			name: string;
			bio: string;
		},
		watchOption: WatchOption | null
	};
	public genres: Genre[];
	public movies: Movie[];
	public snacks: Snack[];
	public watchOptions: WatchOption[];

	// I initialize the app component.
	constructor() {

		this.genres = [
			{ id: "action", name: "Action / Adventure", adultsOnly: false },
			{ id: "commedy", name: "Commedy", adultsOnly: false },
			{ id: "documentary", name: "Documentary", adultsOnly: true },
			{ id: "drama", name: "Drama", adultsOnly: false },
			{ id: "horror", name: "Horror", adultsOnly: true },
			{ id: "scifi", name: "Sci-Fi / Fantasy", adultsOnly: false }
		];

		this.movies = [
			{ id: "tt0092890", name: "Dirty Dancing", releasedAt: "1987" },
			{ id: "tt0103064", name: "Terminator 2", releasedAt: "1991" },
			{ id: "tt0093779", name: "The Princess Bride", releasedAt: "1987" },
			{ id: "tt0098635", name: "When Harry Met Sally", releasedAt: "1989" }
		];

		this.snacks = [
			{ id: "jrmints", name: "Junior Mints" },
			{ id: "pmm", name: "Peanut M&Ms" },
			{ id: "popcorn", name: "Popcorn" },
			{ id: "twizzlers", name: "Twizzlers" }
		];

		this.watchOptions = [
			{ id: "none", label: "I don't watch movies." },
			{ id: "one", label: "Maybe one a week" },
			{ id: "twoish", label: "One to two movies a week" },
			{ id: "lots", label: "At least one movie a day" },
			{ id: "fulltime", label: "I had to quite my job!" }
		];

		this.form = {
			favoriteGenres: {
				action: false,
				commedy: false,
				documentary: false,
				drama: false,
				horror: false,
				scifi: false
			},
			favoriteMovie: null,
			favoriteSnacks: [],
			user: {
				name: "",
				bio: ""
			},
			watchOption: null
		};

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I output the current state of the form view-model.
	public processForm( ngForm: NgForm ) : void {

		console.group( "Form Submission" );
		console.log( JSON.stringify( this.form, null, 4 ) );
		console.log( ngForm );
		console.groupEnd();

	}

}

The main thing to notice here is that the form view-model is composed of complex objects, not simple strings. NgModel - and template-driven forms - are perfectly capable of mapping the simple values in our HTML form inputs onto the complex objects contained within our view-model.

Now, let's look at the template for this form. I've tried to provide sufficient commenting around each form control instance:

<!--
	You don't strictly need a Form when dealing with form inputs. However, since I am
	also using an NgModelGroup for my "user" inputs, I need to provide a "container"
	within which that group will register itself. Plus, the Form gives you access to
	the submit event and the NgForm export reference.
-->
<form #ngForm="ngForm" (submit)="processForm( ngForm )">

	<h3>
		User Profile
	</h3>

	<div ngModelGroup="form.user">

		<p>
			<label>
				<strong>Name:</strong><br />
				<!--
					Text-based inputs are the bread-and-butter of template-driven forms.
					You just bind to the view-model with [(ngModel)] and it just works.
				-->
				<input
					type="text"
					name="name"
					[(ngModel)]="form.user.name"
					size="20"
				/>
			</label>
		</p>

		<p>
			<label>
				<strong>Bio:</strong><br />
				<!--
					Textarea are just a different form of text-based inputs. As such,
					they also work seamlessly with [(ngModel)].
				-->
				<textarea
					name="bio"
					[(ngModel)]="form.user.bio"
					rows="5"
					cols="70"
				></textarea>
			</label>
		</p>

	</div>

	<h3>
		Favorite Movie
	</h3>

	<ul>
		<li *ngFor="let movie of movies">
			<label>
				<!--
					Unlike a text-based input, Radio controls are a bit different in that
					they have both a value AND a "state" (checked). As such, we need to
					provide two references: the current view-model value (form.favoriteMovie)
					and the value of the control (movie). The RadioControlValueAccessor
					directive overrides the [value] input binding of the native control,
					intercepting it and using it to determine if the radio's NgModel
					value matches the [value] reference.
				-->
				<input
					type="radio"
					name="favoriteMovie"
					[(ngModel)]="form.favoriteMovie"
					[value]="movie"
				/>
				{{ movie.name }} ( {{ movie.releasedAt }} )
			</label>
		</li>
		<li>
			<label>
				<!-- We can use a [value]="null" option to clear the radio choice. -->
				<input
					type="radio"
					name="favoriteMovie"
					[(ngModel)]="form.favoriteMovie"
					[value]="null"
				/>
				None / Clear
			</label>
		</li>
	</ul>

	<h3>
		Favorite Genres
	</h3>

	<ul>
		<li *ngFor="let genre of genres">
			<label>
				<!--
					The checkbox control is similar to the radio control in that it has
					both a value and a state (checked); but, unlike the RadioControlValueAccessor,
					the CheckboxControlValueAccessor directive deals exclusively with
					Booleans. As such, it doesn't have to override any [value] input
					binding - the NgModel view-model must reference a Boolean, which
					is how the directive drives the [checked] state.
				-->
				<input
					type="checkbox"
					name="favoriteGenre_{{ genre.id }}"
					[(ngModel)]="form.favoriteGenres[ genre.id ]"
				/>
				{{ genre.name }}
			</label>
		</li>
	</ul>

	<h3>
		Favorite Snacks
	</h3>

	<p>
		<!--
			Just as with the Radio control, the Select control seeks to match the view-
			model value with a set of possible values. As such, we need to provide two
			references: the current view-model value (form.favoriteSnacks) and the value
			of the option (snackOption). However, unlike the Radio control, the Select
			control uses [ngValue] to provide the option value in its child elements.
			--
			NOTE: Since we're using "multiple", the selected values are collected in
			an Array.
		-->
		<select
			name="favoriteSnacks"
			[(ngModel)]="form.favoriteSnacks"
			multiple
			size="5">
			<option *ngFor="let snackOption of snacks" [ngValue]="snackOption">
				{{ snackOption.name }}
			</option>
		</select>
	</p>

	<h3>
		Movie Watching Behavior
	</h3>

	<p>
		<!--
			The single Select works just like the "multiple" Select; except, it doesn't
			gather the options in an Array - it just binds the selected option [ngValue]
			to the view-model (form.watchOption).
		-->
		<select name="watchOption" [(ngModel)]="form.watchOption">
			<option [ngValue]="null">
				Please select an option.
			</option>
			<option *ngFor="let option of watchOptions" [ngValue]="option">
				{{ option.label }}
			</option>
		</select>
	</p>

	<p>
		<button type="submit">
			Submit Form
		</button>
	</p>

</form>


<hr />

<h2>
	Form View-Model Current State
</h2>

<pre><code>{{ form | json }}</code></pre>

As you can see, the general rules around NgModel are as follows:

  • Text-inputs just need [NgModel].
  • Radio buttons need [NgModel] and [value].
  • Checkboxes need [NgModel] and only work with Booleans.
  • Single selects need [NgModel] and [NgValue].
  • Mulit-selects need [NgModel] and [NgValue] and aggregate Arrays.

Now, if we run this page and fill out the form, we can see that the form's view-model contains all of the appropriate complex-object data:

NgModel and template-driven form reference in Angular 7.2.13 - perfectly capable of binding to complex objects.

Of course, this just covers the built-in Control Value Accessors; as developers we can create our own custom value accessors in order to provide additional NgModel functionality. For example, we can create a Control Value Accessor for the input type="file" that synchronizes the FileList of the input, not the file path.

Like I said at the beginning, this is primarily a note-to-self so that when I forget how NgModel works with Select, I have something to reference. Of course, if - like me - you use text-inputs in the majority of your forms, I hope that this reference can also provide value for you in the future.

Want to use code from this post? Check out the license.

Reader Comments

447 Comments

This is awesome. I like the way you can just add objects to the value of a radio input. It would be cool, if you could do the same thing for checkboxes and then the form just returns an array of objects for those that are checked...

I wonder how this all works for a Reactive Form? I presume it follows the same paradigm, with respect to the fact that objects can be used for values?

15,841 Comments

@Charles,

It's funny you mention the Array and Checkbox idea -- that was the last thing that I tested right before I posted this. And, I couldn't get it to work in the way you were hoping -- that it would just add items to an array. I agree that it would be cool; but, from what I could see, it only works with Boolean values. This could just be something I was misunderstanding. But, when looking at the Control Value Accessor for the checkbox, the underlying code seems to just inspect the value as a Boolean.

Re: Reactive Forms, I am not entirely sure. As you know, I'm very new to the Reactive Forms, so I can't really speak with confidence. I assume they work similarly.

447 Comments

I might see if I can add an object to the value of a Reactive form control. I will let you know the outcome.

It just seems weird, that it can only return a Boolean, seeing as Radio controls are essentially linked checkboxes.

As well as this, a standard HTML checkbox can return primitive values like strings & numbers, as well as Booleans. I guess, internally, all Angular has to do is to store the value as a JSON string and then parse it into an object, if a checkbox is defined [checked]:

https://www.w3schools.com/tags/att_input_type_checkbox.asp

15,841 Comments

@Charles,

Yeah, especially considering that in a dynamic form, it's easy to imagine a list of checkbox being rendered based on a queried data-set. Like user preferences kind of a thing:

<div *ngFor="let pref of userPrefs">
	<input type="checkbox" ..... /> {{ pref.name }}
</div>

I'll be curious to hear if you find anything else about the Reactive Form version.

I'm also wondering if there's a way we could somehow change the behavior with a custom ControlValueAccessor. But, not sure that is possible.

447 Comments

Hi Ben. I think I have cracked it, using a custom:

CheckboxControlValueAccessor:

Its not quite as clean as I would like, because I had to resort to using a counter to calculate when the checkbox is checked or not.

But, it works, never-the-less!

https://codesandbox.io/s/jnxn34o41w?fontsize=14
15,841 Comments

@Charles,

I keep going back over this issue in my head and I think, ultimately, what keeps tripping me up is the fact that if multiple checkboxes with same name were stored in an Array, then Angular would have to check to see if a reference existed within an Array on each change digest. It seems like it may not be a very efficient workflow.

15,841 Comments

@Charles,

Right, sorry -- I was just thinking out loud about why Angular might not want to use this approach by default.

447 Comments

OK. You are correct. According to W3c, checkboxes can have the same name, but only the value of the last checkbox would be submitted, so essentially each checkbox, with the same name, would overwrite the value of the previous one.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel