20 Angular Concepts you Need to Know
Angular had its ups and downs and went through more major reworks than I could count, but what you might not realize is that it might be the wisest choice for both small startups and large enterprise projects.
Its latest version is the best one yet, so let’s see modern Angular in action by reviewing 20 of its most important features while creating a todo app.
1. CLI
We’ll start a new project using the Angular CLI, which offers a wide range of commands to automate repetitive tasks and enforce best practices.
$ ng new todo
2. Components
Angular apps are built from independent components, which are simple classes annotated with the @Component decorator.
Here we’ll define the custom tag used to insert the component into a template.
@Component({
selector: 'app-root',
})
3. Modules
Angular comes packed with a powerful module system that allows developers to structure their application into cohesive blocks of functionality, making it easier to manage, maintain, and scale the codebase.
4. Standalone Components
However, we can opt out from this constraint by marking the component as standalone.
@Component({
selector: 'app-root',
standalone: true
})
5. Imports
Then, we’ll use the imports array to declare components and modules our code might depend on and then link the template and styling files associated with this class.
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, TaskComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
6. Single File Components
What’s interesting is that while having separate files for component class, template, and styles is the most common way to structure components, you can also define Single file components with the template and styles properties.
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, TaskComponent],
template: `<h1>Awesome Angular</h1>`,
styleUrl: ['h1 { font-weight: normal; }']
})
7. Signals
Ok, moving to something a bit more involved, reactivity is the magic behind any modern web app. In Angular, Signals are used to keep track of internal state.
@Component({
selector: "app-root",
standalone: true,
imports: [CommonModule, TaskComponent],
templateUrl: "./app.component.html",
styleUrl: "./app.component.scss",
})
export class AppComponent {
tasks = signal<Task[]>([]);
}
8. Computed
Whenever signals are changed, computed values and the associated DOM elements are updated.
@Component({
selector: "app-root",
standalone: true,
imports: [CommonModule, TaskComponent],
templateUrl: "./app.component.html",
styleUrl: "./app.component.scss",
})
export class AppComponent {
tasks = signal<Task[]>([]);
count = computed(() => this.tasks().length);
doneCount = computed(() => this.tasks().filter((it) => it.done).length);
}
9. Lifecycle Hooks
We’ll use the on init lifecycle hook to load the tasks when the component is first mounted in the dom.
@Component({
selector: "app-root",
standalone: true,
imports: [CommonModule, TaskComponent],
templateUrl: "./app.component.html",
styleUrl: "./app.component.scss",
})
export class AppComponent {
tasks = signal<Task[]>([]);
count = computed(() => this.tasks().length);
doneCount = computed(() => this.tasks().filter((it) => it.done).length);
async ngOnInit() {
this.tasks.set(await fetchTasks());
}
}
10. Templates
Then we can jump into the HTML file to define our template.
<h1>{{doneCount() }} / {{count()}} Tasks</h1>
11. Built-in control flow
We can render elements in the page with the double brackets syntax, and use the built-in control flow to conditionally render or display lists of elements.
<h1>{{doneCount() }} / {{count()}} Tasks</h1>
@for (task of tasks(); track task.id) {
<div [ngClass]="{'done': task.done}">
<input type="checkbox" (change)="toggleClicked(task)" />
<h6>{{ task.text }}</h6>
</div>
}
12. Data binding
We can bind component data to element attributes using square brackets.
<h1>{{doneCount() }} / {{count()}} Tasks</h1>
@for (task of tasks(); track task.id) {
<div [ngClass]="{'done': task.done}">
<input type="checkbox" (change)="toggleClicked(task)" />
<h6>{{ task.text }}</h6>
</div>
}
<footer>
<input [(ngModel)]="text" />
</footer>
13. Event listeners
We can register event listeners for task saving and toggling using parentheses.
<h1>{{doneCount() }} / {{count()}} Tasks</h1>
@for (task of tasks(); track task.id) {
<div [ngClass]="{'done': task.done}">
<input type="checkbox" (change)="toggleClicked(task)" />
<h6>{{ task.text }}</h6>
</div>
}
<footer>
<input [(ngModel)]="text" />
<button (click)="save()">Save</button>
</footer>
Let’s jump back into the class component to define the listeners.
In the save function we’ll simply push a new entry in the task list, and then update the signal value. Note that Angular is tracking references to detect signal changes, so we’ll use array destructuring to get a new array reference.
@Component({
selector: "app-root",
standalone: true,
imports: [CommonModule, TaskComponent],
templateUrl: "./app.component.html",
styleUrl: "./app.component.scss",
})
export class AppComponent {
tasks = signal<Task[]>([]);
count = computed(() => this.tasks().length);
doneCount = computed(() => this.tasks().filter((it) => it.done).length);
async ngOnInit() {
this.tasks.set(await fetchTasks());
}
save() {
this.tasks.set([
...this.tasks(),
{
id: crypto.randomUUID(),
text: this.text(),
done: false,
},
]);
this.text.set("");
}
}
This is why we’ll follow the same approach to force a change detection when the task status is updated.
Ok, next, Angular offers a handful of interesting features that will allow us to improve our codebase.
By the way, you are following The Snippet, the fast-paced, no BS series where we are reading code to get better at writing code.
14. Reactive Forms
We can use Reactive Forms to better capture user input. We need to first import this module in our component, and then define a Form Group.
@Component({
selector: "app-root",
standalone: true,
imports: [CommonModule, ReactiveFormsModule, TaskComponent],
templateUrl: "./app.component.html",
styleUrl: "./app.component.scss",
})
export class AppComponent {
tasks = signal<Task[]>([]);
count = computed(() => this.tasks().length);
doneCount = computed(() => this.tasks().filter((it) => it.done).length);
async ngOnInit() {
this.tasks.set(await fetchTasks());
}
save() {
this.tasks.set([
...this.tasks(),
{
id: crypto.randomUUID(),
text: this.text(),
done: false,
},
]);
this.text.set("");
}
}
15. Dependency Injection
The group is initialized in the constructor, where we’ll have access to a helper through dependency injection.
@Component({
selector: "app-root",
standalone: true,
imports: [CommonModule, ReactiveFormsModule, TaskComponent],
templateUrl: "./app.component.html",
styleUrl: "./app.component.scss",
})
export class AppComponent {
tasks = signal<Task[]>([]);
count = computed(() => this.tasks().length);
doneCount = computed(() => this.tasks().filter((it) => it.done).length);
constructor(private builder: FormBuilder) {
this.form = this.builder.group({
text: [""],
});
}
async ngOnInit() {
this.tasks.set(await fetchTasks());
}
save() {
this.tasks.set([
...this.tasks(),
{
id: crypto.randomUUID(),
text: this.text(),
done: false,
},
]);
this.text.set("");
}
}
Next, in the template, we’ll replace our footer with a reactive form. Once the form group and the form control are bound, we can jump back into the save function and make the necessary changes.
<h1>{{doneCount() }} / {{count()}} Tasks</h1>
@for (task of tasks(); track task.id) {
<div [ngClass]="{'done': task.done}">
<input type="checkbox" (change)="toggleClicked(task)" />
<h6>{{ task.text }}</h6>
</div>
}
<form [formGroup]="form">
<input formControlName="text" />
<button type="button" (click)="save()">Save</button>
</form>
While this might not look like much in our small demo, reactive forms will give you better control over data and validation in complex projects.
@Component({
selector: "app-root",
standalone: true,
imports: [CommonModule, ReactiveFormsModule, TaskComponent],
templateUrl: "./app.component.html",
styleUrl: "./app.component.scss",
})
export class AppComponent {
tasks = signal<Task[]>([]);
count = computed(() => this.tasks().length);
doneCount = computed(() => this.tasks().filter((it) => it.done).length);
constructor(private builder: FormBuilder) {
this.form = this.builder.group({
text: [""],
});
}
async ngOnInit() {
this.tasks.set(await fetchTasks());
}
save() {
this.tasks.set([
...this.tasks(),
{
id: crypto.randomUUID(),
text: this.form.get("text")?.value,
done: false,
},
]);
this.text.set("");
}
}
We can also extract the task line view in a separate component, and pass in the object and the toggle handler as properties.
16. Deferrable Views
Since the task line might grow in size, we have the option to defer its rendering which will improve the initial loading of our app, by splitting the JS served to the browser into multiple smaller chunks.
<h1>{{doneCount() }} / {{count()}} Tasks</h1>
@defer { @for (task of tasks(); track task.id) {
<task-component [task]="task" (toggle)="toggleClicked($event)" />
} } @placeholder {
<p>Your tasks...</p>
} @loading {
<p>Your tasks...</p>
}
<form [formGroup]="form">
<input formControlName="text" />
<button type="button" (click)="save()">Save</button>
</form>
17. Input
In the child component we can receive properties from parents via inputs.
// Task.component.ts
@Component({
selector: "task-component",
standalone: true,
imports: [CommonModule],
templateUrl: "./task.component.html",
})
export class TaskComponent {
@Input() task: Task | null = null;
}
18. Output
We send information back to the outside scope via outputs.
// Task.component.ts
@Component({
selector: "task-component",
standalone: true,
imports: [CommonModule],
templateUrl: "./task.component.html",
})
export class TaskComponent {
@Input() task: Task | null = null;
@Output() toggle = new EventEmitter<Task>();
}
19. Pipes
We can also use Pipes which have to be imported, and then added in the templates to easily transform the data.
// Task.component.ts
@Component({
selector: "task-component",
standalone: true,
imports: [CommonModule, UpperCasePipe],
templateUrl: "./task.component.html",
})
export class TaskComponent {
@Input() task: Task | null = null;
@Output() toggle = new EventEmitter<Task>();
}
<!-- Task.component.html -->
@if (!!task) {
<div [ngClass]="{'done': task.done}">
<input type="checkbox" (change)="onToggle()" [checked]="task.done" />
<h6>{{ task.text | uppercase}}</h6>
</div>
}
20. Event emitters
Whenever a task is marked as done, the onToggle function will be called and an event will be emitted.
// Task.component.ts
@Component({
selector: "task-component",
standalone: true,
imports: [CommonModule, UpperCasePipe],
templateUrl: "./task.component.html",
})
export class TaskComponent {
@Input() task: Task | null = null;
@Output() toggle = new EventEmitter<Task>();
onToggle() {
this.toggle.emit(this.task!);
}
}
If you like this fast-paced style but want a deeper dive into frontend concepts, take a look at my Yes JS course, or check out one of my other videos interesting as well.
Until next time, thank you for reading!