- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Building Angular Shop Components
You are building 'NgShop', a compact Angular storefront. The shop displays a catalogue of products, lets users adjust quantities with a range slider, and tracks their cart through a shared inventory service. Each lab step adds a working layer: first the product list with control-flow, then pipe-formatted data, then a reusable card component with projection, then a model-bound slider, and finally the reactive cart service that ties everything together.
Lab Info
Table of Contents
-
Challenge
Step 1: Tour the NgShop starter
Welcome to NgShop
In this lab you will build a compact Angular storefront called NgShop step by step. Each step adds one cohesive layer — control-flow rendering, pipe-based formatting, a reusable card component, a model-bound slider, and finally a reactive cart service — until you have a complete mini-shop that exercises every major Angular template and component pattern.
Before writing any code, take a few minutes to explore the starter project so you know where everything lives. ### Project layout
src/ main.ts # owned by the toolchain — do not edit App.ts # root component shell data/ products.ts # PRODUCTS array, Product interface, CartItem interface product-list.component.ts product-card.component.ts category-label.pipe.ts quantity-slider.component.ts cart.service.tsOpen
src/data/products.tsfirst. You will see theProductinterface (fields:id,name,price,stock,category) and thePRODUCTSarray that seeds the catalogue. TheCartIteminterface (productId,quantity) defines what the cart service stores.Next, open
src/App.ts. It is a minimal standalone component that currently renders nothing useful — you will wire it up in Step 5.Every file you will edit already exists with stub comments marking each
TODO. The tests live intests/and are read-only; they import from your source files and assert on rendered output or returned values. ### Key Angular 22 patterns used in this lab| Pattern | Where you'll use it | |---|---| |
@for+track+$index| Step 2 — product list | |@if/@else| Step 2 — stock badge | | Built-in pipes (CurrencyPipe,UpperCasePipe) | Step 3 — price & name formatting | | CustomPipeTransform| Step 3 — category labels | |ng-content+ named slots | Step 4 — card shell | |input()signal | Step 4 — typed card input | |output()function | Step 4 — addToCart event | |model()+computed()| Step 5 — quantity slider | |inject()+ singleton service | Step 5 — cart service |All components in this lab are standalone — no
NgModule, noBrowserModule, nodeclarationsarray anywhere. You are ready to move to Step 2. ## Solution filesinfo> If you get stuck on a task, a reference solution is available. Solution files live in the
solution/folder of your workspace, grouped by step (solution/step2/throughsolution/step5/). Each file is named<file>-<step>-<task>.ts, sosolution/step2/product-list.component-2-1.tsholds the solution for Task 1 in Step 2.Try each task on your own first, then compare your work against the matching solution file. Opening a solution file does not change the code you are working on, so copy over only what you need.
-
Challenge
Step 2: Render the product catalogue with control flow
Step 2: Render the product catalogue with control flow
Angular's built-in control flow (
@for,@if) replaced the*ngForand*ngIfstructural directives. The new syntax is part of the template language itself — no import required — and it requires an explicittrackexpression so Angular can match DOM nodes to data items efficiently.In this step you will work entirely inside
src/product-list.component.ts. You will add the@forloop that renders the catalogue, expose the$indexalias so position information is available in the template, then add the@if/@elsebranch that shows either the stock count or an 'Out of stock' badge, and finally expose the catalogue data on the component class so the page renders real products. ### @for, track, and scoped alias variablesThe
@forblock iterates an iterable and requires atrackexpression that must uniquely identify each item. Using a stable ID (rather than$index) prevents Angular from destroying and recreating DOM nodes when items are reordered.Alias variables like
$index,$first, and$lastare declared inline withlet:@for (item of items; track item.id; let i = $index) { <li>{{ i }}: {{ item.name }}</li> }Task 2.1 uses this syntax to render the catalogue. The component's
productsarray is still empty at this point, so the tests supply their own sample products; you connect the real catalogue in Task 2.3. ### @if and @elseConditional rendering works with
@ifand an optional@elseblock placed immediately after the closing brace:@if (item.stock > 0) { <span class="in-stock">{{ item.stock }} in stock</span> } @else { <span class="out-of-stock">Out of stock</span> }The two branches are mutually exclusive and each can contain any template content. The loop and the stock badge now render whatever is in the
productsarray, but the component's array is still empty, so the page has nothing to show. The last task gives the component its data. -
Challenge
Step 3: Format shop data with built-in and custom pipes
Step 3: Format shop data with built-in and custom pipes
Pipes transform display values without mutating component state. Angular ships several built-in pipes; you can also write your own by implementing
PipeTransform.In this step you will apply two built-in pipes to the product list template, then implement a custom pipe that converts internal category key strings into human-readable labels. ### Built-in pipes in standalone components
Because there is no shared module, every pipe a standalone component uses must appear in the component's own
importsarray:@Component({ standalone: true, imports: [CurrencyPipe, UpperCasePipe], template: `{{ price | currency }} — {{ name | uppercase }}` })CurrencyPipeformats a number as a locale currency string (default: USD).UpperCasePipetransforms a string to all-caps. ### Authoring a pure custom pipeA custom pipe is a class decorated with
@Pipethat implementsPipeTransform:@Pipe({ name: 'myPipe', pure: true, standalone: true }) export class MyPipe implements PipeTransform { transform(value: string): string { // return the formatted value } }Setting
pure: true(the default) tells Angular to calltransformonly when the input reference changes, keeping rendering fast. The stub insrc/category-label.pipe.tsalready has the decorator and class shell — you only need to fill intransform. -
Challenge
Step 4: Extract a reusable ProductCard with projection, inputs, and outputs
Step 4: Extract a reusable ProductCard with projection, inputs, and outputs
A well-structured component library separates data (what to display) from structure (how to lay it out).
ProductCardComponentwill be the layout shell: it receives a product via a typed input signal, projects arbitrary body content throughng-content, exposes a named slot for action buttons, and emits anaddToCartevent when the button is clicked. ### Signal inputs with input()Angular 22 uses signal-based inputs instead of the
@Input()decorator:import { input } from '@angular/core'; export class MyComponent { // Optional with default label = input<string>('default'); // Required — throws if parent omits it item = input.required<Product>(); }The value is read by calling the signal:
this.item()in the class,item()in the template. ### Content projection and named slotsng-contentlets a parent inject arbitrary markup into a child's template. A plain<ng-content />accepts all projected nodes. Add aselectattribute to filter by a CSS selector:<!-- card template --> <div class="card-body"> <ng-content /> </div> <div class="card-actions"> <ng-content select="[slot=actions]" /> </div><!-- parent template --> <app-product-card> <p>Body content here</p> <button slot="actions">Buy</button> </app-product-card>Nodes without a matching
slotattribute fall into the default slot. ### Signal outputs with output()The signal-based
output()function replacesnew EventEmitter():import { output } from '@angular/core'; export class MyComponent { itemSelected = output<string>(); select(id: string) { this.itemSelected.emit(id); } }Parents bind to the output with the same
(eventName)syntax they used before. -
Challenge
Step 5: Wire the quantity slider and connect the reactive cart service
Step 5: Wire the quantity slider and connect the reactive cart service
This final step ties the whole application together. You will implement two-way model binding on
QuantitySliderComponent, build the reactive cart service, wire theAppcomponent so the slider sets how many items each 'Add to cart' click adds, and let the cart remove a product while keeping the grouped view consistent. ### Two-way model binding with model()model()creates a writable signal that supports the[( )]banana-in-a-box syntax from the parent:// child component quantity = model<number>(1);<!-- parent template --> <app-quantity-slider [(quantity)]="myQty" />When the child calls
this.quantity.set(n), the parent'smyQtyupdates automatically. Pair it with acomputed()to derive a validated read-only value without touching the raw model signal. ### Singleton services with inject() and computed()Services decorated with
@Injectable({ providedIn: 'root' })are singletons available anywhere in the app. Inject them with theinject()function:import { inject } from '@angular/core'; import { MyService } from './my.service'; export class MyComponent { private svc = inject(MyService); }Inside a service you can use
signal()for mutable state andcomputed()to derive aggregated views of that state. Computed signals are lazy and memoised — they only recalculate when a dependency signal changes. WithCartServiceable to add and group items, the next piece is connecting theAppcomponent so the slider and the product list reach the service. ### Removing items from a signal-backed listA signal holds a snapshot, so you change a list by replacing it instead of editing it in place.
Array.prototype.filter()returns a new array without the entries you want gone, andupdate()stores it:this._items.update(items => items.filter(item => item.id !== id));Every computed signal that reads
_itemsthen recalculates, which is why adding and removing keep the same derived view consistent. ### Congratulations!You have built a complete Angular storefront from scratch. Here is a summary of what each layer contributes:
- Step 2 —
@forwithtrackand$indexrender the catalogue efficiently,@if/@elsesurface stock state. - Step 3 —
CurrencyPipeandUpperCasePipeformat display values;CategoryLabelPipekeeps mapping logic out of the component class. - Step 4 —
ProductCardComponentseparates layout from data usingng-contentslots, a typedinput(), and a typedoutput(). - Step 5 —
model()+computed()enforce the slider's minimum value;CartServiceholds a mutable signal and a derived grouped view that stays consistent when items are added or removed;inject()makes the singleton available everywhere;[(quantity)]keeps the slider and theAppsignal in sync.
These patterns compose cleanly at any scale — the same techniques appear in production Angular applications with hundreds of components.
- Step 2 —
-
Challenge
Step 6: Run the application
Every task is done, and each one was graded the moment you completed it. What you have not done yet is watch the whole application run at once -- which is the only place the pieces you wrote separately become a single working program.
Start the application
Open the Terminal tab and start the dev server:
npm run devIt serves the application on port 4200. Switch to the Web Browser tab to use it: the page is running the functions you implemented in the previous steps, so what you see is your own code rather than a demonstration of it.
Leave the server running while you look around. It reloads on save, so you can change a file and see the result without restarting anything. ### Run the checks yourself
The lab ran each task's test for you as you went, but they are ordinary tests and you can run them whenever you like. The whole suite:
npx --no-install ng test --no-watch --reporters=dotsOr one task at a time:
bash runTest.sh "task 2.1"Each failure names the task it belongs to, so the output tells you which step to go back to rather than which line to stare at. Two things worth trying before you finish: break something on purpose and watch the matching check fail, then put it back. Knowing what a failure looks like is worth as much as knowing what a pass looks like, and it costs you nothing here.
When you are done, everything you wrote is still in the editor tabs -- the lab is yours to keep reading.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.