Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

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 platform
Lab Info
Level
Beginner
Last updated
Oct 23, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. 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.ts
    

    Open src/data/products.ts first. You will see the Product interface (fields: id, name, price, stock, category) and the PRODUCTS array that seeds the catalogue. The CartItem interface (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 in tests/ 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 | | Custom PipeTransform | 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, no BrowserModule, no declarations array anywhere. You are ready to move to Step 2. ## Solution files

    info> 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/ through solution/step5/). Each file is named <file>-<step>-<task>.ts, so solution/step2/product-list.component-2-1.ts holds 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.

  2. 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 *ngFor and *ngIf structural directives. The new syntax is part of the template language itself — no import required — and it requires an explicit track expression 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 @for loop that renders the catalogue, expose the $index alias so position information is available in the template, then add the @if / @else branch 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 variables

    The @for block iterates an iterable and requires a track expression 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 $last are declared inline with let:

    @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 products array 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 @else

    Conditional rendering works with @if and an optional @else block 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 products array, but the component's array is still empty, so the page has nothing to show. The last task gives the component its data.

  3. 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 imports array:

    @Component({
      standalone: true,
      imports: [CurrencyPipe, UpperCasePipe],
      template: `{{ price | currency }} — {{ name | uppercase }}`
    })
    

    CurrencyPipe formats a number as a locale currency string (default: USD). UpperCasePipe transforms a string to all-caps. ### Authoring a pure custom pipe

    A custom pipe is a class decorated with @Pipe that implements PipeTransform:

    @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 call transform only when the input reference changes, keeping rendering fast. The stub in src/category-label.pipe.ts already has the decorator and class shell — you only need to fill in transform.

  4. 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). ProductCardComponent will be the layout shell: it receives a product via a typed input signal, projects arbitrary body content through ng-content, exposes a named slot for action buttons, and emits an addToCart event 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 slots

    ng-content lets a parent inject arbitrary markup into a child's template. A plain <ng-content /> accepts all projected nodes. Add a select attribute 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 slot attribute fall into the default slot. ### Signal outputs with output()

    The signal-based output() function replaces new 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.

  5. 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 the App component 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's myQty updates automatically. Pair it with a computed() 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 the inject() 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 and computed() to derive aggregated views of that state. Computed signals are lazy and memoised — they only recalculate when a dependency signal changes. With CartService able to add and group items, the next piece is connecting the App component so the slider and the product list reach the service. ### Removing items from a signal-backed list

    A 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, and update() stores it:

    this._items.update(items => items.filter(item => item.id !== id));
    

    Every computed signal that reads _items then 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 — @for with track and $index render the catalogue efficiently, @if/@else surface stock state.
    • Step 3 — CurrencyPipe and UpperCasePipe format display values; CategoryLabelPipe keeps mapping logic out of the component class.
    • Step 4 — ProductCardComponent separates layout from data using ng-content slots, a typed input(), and a typed output().
    • Step 5 — model() + computed() enforce the slider's minimum value; CartService holds 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 the App signal in sync.

    These patterns compose cleanly at any scale — the same techniques appear in production Angular applications with hundreds of components.

  6. 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 dev
    

    It 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=dots
    

    Or 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

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

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.

Get started with Pluralsight