Course outline

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.

About 5 min readContent reviewed
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.

typescript A complete owner transition model
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"); }
Expected output
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.

typescript Contextual child with required data and typed intent
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.

html Contextual parent binding after importing CourseOption
<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.

Official references