Angular Component – Part 1: one-way binding vs. two-way binding

In this short blog series, I want to show how to create an Angular component that can be used for user inputs.

I know this is a topic for which many articles have been published. But while there are great tutorials that explain how it’s done, many of them lack the answer to the question of why. This is the reason I’m describing in this series how and why we are writing our components the way we do here at TimeRocket.

This will be a 5-part series on the topic, so bear with me if the component doesn’t meet your standards right from the beginning. My approach is to incrementally iterate towards a solution I would consider a good fit for the Angular paradigm.

A simple input component

In part 1, we want to create a simple 24-hour daytime input (because that’s what we do at TimeRocket). This input component shall provide the outer component (from now on, I will call it the view) with the inputs the user made and display the data the view sets on it. To keep things simple, we’re working with this data model:

type DayTime = { hours: number, minutes: number }

The first step is to pass an instance of this type into our component we call time-input. Our component should then read the data of the DayTime instance and display it to the user. So, we need a first draft of time-input:

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { DayTime } from './typeDefinitions';

@Component({
  selector: 'time-input',
  template: `
    <input [ngModel]="time.hours" name="hours" type="number" />
    <input [ngModel]="time.minutes" name="minutes" type="number" />
  `
})
export class TimeInputComponent  {
  @Input() time: DayTime;
}

I’ve decided, for the sake of simplicity, to embed the template into the typescript file so I can show the two parts of the components in just one code snippet. You don’t have to do that, and many tutorials encourage you two split up the logic part from the template. I stick to what every consultant would say in this situation: “It depends.” It depends on the size of your component. For now, our component is very compact, so having both parts in the same file won’t hurt readability.

As you may have noticed, I didn’t include a styleUrls or styles property in the @Component annotation. In this whole series, I will only focus on angular logic, so I won’t bother styling the entire thing as this would only distract from the primary goal of this tutorial.

Another thing, especially to JavaScript newcomers, is the backtick (`) I used to write my template code. The backtick allows us to write multiline strings (in this case, HTML code) and gives us a nice and easy way to perform string interpolation:

const name = 'Bob';
const hello = `Hello ${name}`;

 

Using the component

Next in our agenda is using our component by passing in a value:

import { Component } from '@angular/core';
import { DayTime } from './typeDefinitions';
 
@Component({
  selector: 'my-app',
  template: `<time-input [time]="timeInMyApp"></time-input>`
})
export class AppComponent  {
  timeInMyApp: DayTime = { hours: 8, minutes: 30 }
} 

The @Input() annotation created a sort of getter attribute on our component, letting us pass in the value timeInMyApp from AppComponent (our view) into the time field of TimeInputComponent. This concept is called property binding. It’s a unidirectional binding from a view to the component.

What about user inputs?

So far, so good: The initial time we set in AppComponent is displayed via the TimeInputComponent. But how can we notify AppComponent about changes made by the user? As you might already have guessed: If there is an @Input(), an @Output() can’t be too far!

@Component({
  selector: 'time-input',
  template: `
    <input [ngModel]="time.hours" name="hours" type="number" />
    <input [ngModel]="time.minutes" name="minutes" type="number" />
    <button type="button" name="addHour" (click)="onAddHourButtonClick()">Add hour</button>
  `
})
export class TimeInputComponent  {
  @Input() time: DayTime;
  @Output() timeHasChanged = new EventEmitter<DayTime>();
 
  onAddHourButtonClick() {
    const newValue: DayTime = {
      hours: this.time.hours + 1,
      minutes: this.time.minutes
    };
    this.timeHasChanged.emit(newValue);
    this.time = newValue;
  }
} 

For demo purposes, I’ve added an “AddHour” button to the template that calls the method onAddHourButtonClick. In this method, we create a new instance of DayTime with the hour property increased by one, then we call emit(...) on timeHasChanged, signaling this @Output() field can propagate a new value. In the end, we set the new value to the time field to display the changed hour value to the user via our component.

Now, we have to listen to this time-input changes in our view:

@Component({
  selector: 'my-app',
  template: `
    <time-input [time]="timeInMyApp" (timeHasChanged)="timeInMyApp=$event"></time-input>
    <pre>{{ timeInMyApp | json }}</pre>
  `
})
export class AppComponent  {
  timeInMyApp: DayTime = { hours: 8, minutes: 30 }
} 

In contrast to the square brackets used for input attributes, parantheses are used for output attributes. Every time the emit method gets called on the timeHasChanged field of the TimeInputComponent, the code followed by the equal sign of the (timeHasChanged) attribute gets executed. The magic $event is set by angular and is the exact value passed into the emit method of your EventEmitter (in our case, the timeHasChanged field).

The EventEmitter we use for timeHasChanged emits a stream of events to the outside (to everyone that’s listening). Therefore listening to an EventEmitter is called event binding.

I’ve added a <pre> HTML element to display the current value of timeInMyApp, so we’re sure our input-output mechanism works, and as we can see: it does!

Two-way binding

Until now, we used property binding to pass in values into time-input and event binding to get values out of it. Both of these bindings are unidirectional (or less fancy: one-way). I think this helps to reason about data flow (A component that takes inputs and returns outputs isn’t that far off from a function with parameters and a return value). It’s also a good starting point for writing a component.

However, from a consumer perspective, using our time-input component could definitely be more straightforward: I have to use two different attributes with unguessable names [time] and (timeHasChanged) and to make things worse, there is a magical $event variable. Don’t get me wrong: It’s not bad from a consumer perspective to exactly know what goes in and what comes out, especially if the use case is a bit more complex than in our scenario, but in such a simple case like the one illustrated here, we might also want a simple solution. So, two-way binding to the rescue!

This time I begin by illustrating how the view should look like at the end:

<time-input [(time)]="timeInMyApp"></time-input> 

We want to use the so-called banana in the box [()] syntax (our friends from North America often call it the football in the box syntax, but for most of us living on the old continent, a football is round and not an ellipse [or to be more precise: a prolate spheroid]). We’re now only addressing one attribute and therefore only have one binding, but this time the binding goes both ways.

To let consumers use this syntax, we have to make a subtle change to our TimeInputComponent:

@Output() timeChange = new EventEmitter<DayTime>(); 

Our @Output() EventEmitter has to be called the same as the @Input() field plus “Change.” So, we substituted the $event magic with some angular naming convention. A good trade-off if you ask me.

After this change, we can use the banana in the box syntax, and we have completed the first iteration for our component.

What’s next?

That’s it for part 1 of this mini-series about angular components. Stay tuned for the second part, where we learn how and why to build our component compatible with the ngModel convention.

About the author

Domenic Helfenstein

1 comment

Recent Posts