Presentation Models: Domain-Agnostic UI Contracts In short: A presentation model contains primitives, literal unions, and already-formatted strings. If the component has to decide anything, the model is not finished. The previous lesson established that shared components need their own input shape. This one is about designing that shape so it genuinely has no business meaning left in it. Two rules 1. No backend vocabulary. No , , , or in the name. No database column names like . No business nouns at all — that is the Chapter 4 lint rule. 2. Flat, and already decided. The component should not traverse nested objects, format numbers, parse dates, or evaluate thresholds. Every value arrives ready to render. The anatomy Look at what is absent: no timestamps to format, no thresholds to evaluate, no nested objects, no ids the component would have to interpret. Every business decision was made before this object existed. is worth dwelling on. The component does not know what "Grade A" or "Awaiting lab" mean — it knows maps to , which resolves through the tokens from Chapter 6. The consumer decides that a rejected batch is ; the library decides what looks like. The logic that disappears Without a presentation model, this ends up in the component: Three business rules are now in the library: 38 degrees is critical, is critical, and the raw status string is what to show the user. Change the threshold and you ship a library release. Add a fourth consumer whose equipment runs hotter and you need a branch. With a presentation model, the component is passive: Note the binding rather than — Angular's current guidance is to use class bindings, and it needs no import. The threshold moved to the adapter in the consuming app, where the team who owns that equipment can change it without asking anyone. Formatted strings, not raw values This is the design decision people most often get wrong, so it is worth being explicit. If the model carries , the component must decide on decimal places, thousands separators, and locale. Those differ by application — the dispatch dashboard shows in , the farmer portal might show for a European co-op. The component cannot know, so it will guess, and it will be wrong for someone. The exception is when the component itself needs the number to do its job — a progress bar needs because it computes a width from it. The rule is: numbers when the component computes with them, strings when it only displays them. Where the model lives In the library, beside the component, exported from : Chapter 4's rule applies: a type appearing in a public input signature is already public, so exporting it is just being honest. Consumers need to type their adapter's return value. Which also means it is a permanent contract . Adding an optional field is a minor change; adding a required one or renaming anything is breaking, exactly like an input. Chapter 9 treats it that way. Designing one Work backwards from the markup. 1. Sketch the component. What text appears? What varies? 2. Give each varying thing a field, named for its role ( , ), never its source ( ). 3. For anything with a colour or emphasis, add a semantic tone rather than a colour. 4. For anything numeric, ask: does the component compute with it, or display it? Compute → number. Display → formatted string. 5. Check every field name against a colleague from a different domain. If they cannot guess what it holds, rename it. Step five is a surprisingly effective filter. fails it. passes. "But I thought this was just extra mapping code" It is genuinely extra code, and someone has to write and maintain it. That cost is real and should not be waved away. What the objection misses is that the mapping is not new work — it is work that was already happening, just scattered. Every one of these decisions has to be made somewhere: displays as "Awaiting lab" That state is amber, not red displays as "12,500 L" The action is disabled while clearance is pending Without an adapter they do not vanish. They spread — a ternary in one template, a pipe in another, a in a third, a helper in a fourth. Nobody deletes them; they just live in six files. Then a fifth screen shows the same delivery and implements the rules slightly differently, and now the dashboard and the detail page disagree about whether a delivery is amber. With an adapter, all four decisions are in one function with a name. Rules scattered Rules in an adapter --- --- --- Where "PENDING LAB → amber" lives 4–6 places 1 Screens can disagree Yes No Testable without rendering No Yes — a pure function Backend renames a field Fails in N templates Fails in 1 function, at compile time Second app reuses the component No Yes Lines of "extra" code 0 — it is spread out 20 per adapter The compile-time row is the one that repays it fastest. When the backend renames , the adapter fails to compile at exactly one line and TypeScript tells you where. Templates fail silently — renders empty, and you find out from a screenshot. There…