Angular Services & Components: Structuring the Conversion Tool
Romaji to Kana Converter: Angular & Express
Chapter 4 · Angular Services & Components: Structuring the Conversion Tool
convert() is finished — Chapters 2 and 3 built a real, tested, framework-agnostic function that
correctly handles the full gojūon grid, yōon, long vowels, sokuon, and the particle exceptions. Everything
Angular-facing so far, though, is still just Chapter 1's placeholder: one component pinging one endpoint.
This chapter is where the app actually gets structured — a real service holding the tool's own state, and
two small components that read and write it, with no HTTP call anywhere yet. That's deliberate: Chapter 5
is where the conversion logic moves behind the real Express API this course exists to build. This chapter's
own job is making sure that move, when it happens, costs nothing.
Why AppComponent Can't Keep Doing Everything
Chapter 1's AppComponent injected ConvertService directly, called
ping() in ngOnInit, and rendered the result inline — reasonable for confirming two
servers can talk to each other, but it doesn't scale to a real conversion tool. A text input, a live output
panel, and (from Chapter 6 onward) probably a conversion history all need to react to the same underlying
state. Cramming that into one component means every new feature touches the same file, and nothing in it
can be reused, tested, or reasoned about on its own.
The fix is the same one this site's own React-based courses reach for with a shared hook or context — in Angular, a singleton service holding the real state, injected wherever it's needed.
Extending ConvertService With Real Conversion State
Chapter 1's own convert.service.ts already exists with one method, ping(). Rather
than starting a second service, this chapter grows it — the same file gains a genuine convert-and-remember
responsibility, built on Angular's own signal primitives:
hiraganaResult and katakanaResult are declared private — a component
can read the current hiragana value through hiragana(), but it can never call
hiraganaResult.set(...) itself, since that field isn't visible outside the class at all.
asReadonly() gives back a genuine read-only signal (calling .set() on it is a
compile-time error), so the only way any conversion result ever changes is by calling
convertText() — exactly one path in, exactly one path out.
convertText(romaji: string): void doesn't return the conversion result at all — it just updates
the service's own signals, and every interested component reads those signals directly. That shape is what
makes Chapter 5 free: when convertText()'s own two-line body is replaced with a real
this.http.post(...).subscribe(...) call that sets the same two signals from the response
instead of from a direct function call, nothing outside this one method changes — not its name, not its
parameters, not a single line in either component built below.
Building the Input Component
Generated the normal way, in its own folder, matching the 2025 file-naming convention from Chapter 1 —
romaji-input.ts, not romaji-input.component.ts:
#romajiField is a template reference variable — it names the raw DOM element so
romajiField.value can be read directly inside the event binding, with no separate Angular-side
signal needed just to mirror what the input box already holds. Every keystroke calls
onInput(), which calls convertText(), which updates the service's own signals —
this component never touches hiragana or katakana at all; it only ever writes.
Building the Output Component
*ngIf, which only works once CommonModule is
imported. The @if/@else block syntax used here is current Angular's own built-in
control flow — compiled directly by the template compiler, with nothing to import at all. It's exactly the
kind of small, real "verified against current documentation" detail this course keeps flagging, since a
tutorial written even a couple of years ago would show the older directive instead.
Composing Everything in app.ts
AppComponent's own job shrinks to exactly one thing: import the two new components and lay
them out. Chapter 1's ngOnInit ping check already did its job proving the two halves of this
stack can talk to each other — it's removed now, since there's nothing left for AppComponent
itself to compute or hold:
Running ng serve and typing into the input box should update the output panel live, on every
keystroke — no styling yet, no debouncing, nothing polished; Chapter 6 is where the actual UI gets built.
This chapter's own goal was structural, and it's already testable: type "sushi" and confirm
すし/スシ appear.
Two Siblings, Zero Direct Wiring
RomajiInputComponent and KanaOutputComponent sit next to each other in
app.html, but neither one knows the other exists. There's no @Input() passing a
value down from a parent, and no @Output() emitting an event back up — both components simply
inject the same singleton ConvertService and talk to it independently.
| Pattern | When It Fits |
|---|---|
| Shared service (used here) | State genuinely belongs to the whole feature, not to one component — any number of components can read or write it without ever knowing about each other |
Parent-child input()/output()/model() | A component is truly nested inside another and only that one relationship needs to pass data down or events up — current Angular's modern replacement for the older @Input()/@Output() decorators |
This app has no genuine parent-child relationship between the two pieces that need to communicate — an
input box and an output panel are siblings, not one nested inside the other — so the shared-service pattern
is the more direct fit. The input()/output()/model() functions are
worth knowing regardless; a future feature that genuinely does nest one component inside another (a
settings panel embedded in the output component, say) would reach for those instead.
What Chapter 5 Changes, and What It Won't
Chapter 5 gives Express a real /api/convert route, replacing today's /api/ping
placeholder. ConvertService.convertText() will change from calling convert()
directly to calling this.http.post(...) and updating the same two signals from inside a
.subscribe() callback instead of on the very next line. That's the entire change. Neither
RomajiInputComponent nor KanaOutputComponent will be touched at all — they were
never written against "a function call" or "an HTTP request," only against "whatever
ConvertService exposes," and that public shape isn't moving.
Hands-On Exercises
Extend ConvertService exactly as this chapter describes (the private writable signals, the public asReadonly() views, the computed hasResult, and convertText()). Without building either component yet, call service.convertText('konnichiwa') directly and confirm service.hiragana() and service.katakana() return the correct real strings afterward, and that service.hasResult() is false before the call and true after it.
📄 View solutionBuild RomajiInputComponent and KanaOutputComponent exactly as this chapter describes, wire them into app.ts and app.html in place of Chapter 1's ping check, and confirm typing "sushi" into the input field correctly displays すし and スシ in the output panel — including confirming the @else placeholder text is what shows before anything has been typed.
📄 View solutionIn your own words, explain why convertText() returning void and updating signals internally is what lets Chapter 5 swap its own implementation for a real HTTP call with zero changes to either component. Then describe specifically what WOULD have had to change in both RomajiInputComponent and KanaOutputComponent if convertText() had instead directly returned a { hiragana, katakana } object for the caller to handle itself.
📄 View solutionChapter 4 Quick Reference
- ConvertService grows up — Chapter 1's ping-only placeholder now holds real conversion state via private writable signals, public asReadonly() views, and a computed hasResult
- convertText(romaji): void — updates the service's own signals rather than returning a value; currently calls Chapter 3's engine directly, in-process
- RomajiInputComponent — a template reference variable (#romajiField) reads the raw input value; every keystroke calls convertText()
- KanaOutputComponent — reads hiragana/katakana/hasResult straight off the service; renders with @if/@else, current Angular's built-in control flow (no CommonModule import needed)
- Shared service over parent-child wiring — the two components are siblings with no direct relationship, so they communicate through the injected service rather than input()/output()/model()
- The real payoff — Chapter 5 can replace convertText()'s own two-line body with a real HTTP call and change nothing else in the app
- Next chapter: The Express API — the real conversion route this course exists to build