Angular / Basic / Lesson 5
Inputs, outputs, and composition
Give components a small explicit contract: receive data, emit intent, and let the owner decide how shared state changes.
On this page
Lesson goals
- Define required signal inputs and typed outputs.
- Keep state ownership separate from child presentation.
- Test input changes and emitted intent through the consuming boundary.
A child should expose a coherent interaction
An input describes data the parent supplies; an output describes an event the child raises. This separates ownership from presentation and avoids a child silently editing an unrelated parent object. Use names that describe domain intent, such as selected, rather than browser mechanics that obscure the operation. A signal input is read through its getter and can participate in computed derivations. Required inputs communicate a construction contract to Angular template checking, but reading them before Angular has assigned a value is still a lifecycle mistake. Keep initialization assumptions visible instead of suppressing missing data with assertions.
Validate the owner’s transition independently
The complete model represents the parent’s selected identifier and validates it before changing state. A typed component output can help checked callers but does not replace the owner’s domain rules or server authorization. The local test selects id two and rejects zero without a browser. It verifies the transition only, not that a child output reaches a parent handler. In a component fixture, additionally set the child inputs and observe the emitted value through the actual binding. Keep validation failure distinct from a missing selection, since those conditions can require different user feedback.
export {};
class SelectionModel {
selectedId: number | undefined;
select(id: number): void {
if (!Number.isSafeInteger(id) || id < 1) { throw new Error("invalid id"); }
this.selectedId = id;
}
}
const owner = new SelectionModel();
owner.select(2);
console.log(owner.selectedId);
try { owner.select(0); } catch { console.log("rejected"); }
2
rejected
The model prints two and rejected. It does not create an Angular component or dispatch an output event.
Declare data and intent at the child boundary
The contextual child uses input.required for a numeric identifier and a title, plus output<number> for selection intent. Its template reads the current input values and emits the identifier on activation. The parent remains responsible for the resulting state change. output is an Angular compiler-recognized initializer API, not an arbitrary general-purpose event bus, and component outputs do not bubble through the DOM like native events. This fragment requires Angular component initialization and a host with the required inputs. Do not instantiate it directly and assume required input getters already contain values.
import { Component, input, output } from "@angular/core";
@Component({
selector: "app-course-option", standalone: true,
template: `<button type="button" (click)="selected.emit(courseId())">{{ title() }}</button>`
})
export class CourseOption {
readonly courseId = input.required<number>();
readonly title = input.required<string>();
readonly selected = output<number>();
}
A valid host supplies courseId and title. Activating its button would emit that identifier; no browser activation is claimed executed here.
Make the consuming template own the connection
A standalone parent must import CourseOption in its imports array before using the selector. The contextual parent template below binds course data and handles the typed output. The handler should update the parent model or delegate to an appropriately scoped service, not reach into a child’s private fields. Content projection is another composition mechanism for presentation slots, but projected content retains its own ownership and dependency context. Avoid creating a component API with many unrelated booleans that permit contradictory modes; a coherent input model or a small set of explicit variants is easier to test.
<app-course-option
[courseId]="course.id"
[title]="course.title"
(selected)="onSelected($event)" />
The parent supplies one course and receives its selected identifier. The parent class, import metadata, and host are prerequisites, so this is not a standalone runnable program.
Test the contract as a consumer uses it
Use a host fixture that supplies required inputs, changes them, and listens to the output. Assert the button name, emitted identifier, and parent state after interaction. A direct class-method test cannot verify template wiring. Required-input diagnostics catch missing bindings in checked templates, but a runtime boundary still needs validation when data arrives from HTTP or another untyped source. Keep output names distinct from native DOM event names where confusion is likely. Document whether a child only emits intent or also performs work, especially when requests can fail. Predictable boundaries keep reusable components useful without turning them into hidden global controllers.
Common errors and fixes
A required input is read before assignment.
- Why it happens
- Component construction was confused with completed input initialization.
- How to fix it
- Read it in a supported reactive or lifecycle context.
- Prevent it next time
- Test through Angular input assignment, not direct construction alone.
The child changes parent data without a clear event.
- Why it happens
- Shared object mutation replaced explicit ownership.
- How to fix it
- Emit intent and let the owner apply the transition.
- Prevent it next time
- Document mutation behavior in the component API.
An output is expected to bubble to a distant ancestor.
- Why it happens
- Angular outputs were treated as native bubbling events.
- How to fix it
- Bind explicitly or use an appropriate shared coordination service.
- Prevent it next time
- Test the actual consuming boundary.
Check your understanding
1. A second course option emits id three. State how the parent model should change and what a host test must observe.
Show worked solution
The parent validates and selects three, replacing the previous selected identifier according to the feature rule. A host test must activate the second child’s button and observe both its output and the resulting parent state, while confirming the first child’s inputs were not mutated. The local SelectionModel test protects only the transition function, so retain both kinds of evidence.
2. The parent receives courseId from unvalidated HTTP data as a string. Does input.required<number>() convert it? Describe the correct boundary.
Show worked solution
No. The required input type helps static checking but is not a runtime decoder for arbitrary response data. Validate and normalize the HTTP value under a documented policy before constructing the course model. Reject malformed or unauthorized identifiers and test that path. Avoid an assertion that merely labels the string as a number.
Key takeaways
- Inputs supply data and outputs express intent.
- Keep state ownership explicit.
- Verify component contracts through their host.