Fanis Prodromou
I am a Senior Software Engineer with a passion for Front End development with Angular. I have developed vast experience in code quality, application architecture, and application performance.
Fanis Prodromou
Code. Teach. Community. Angular.
https://blog.profanis.me
/prodromouf
@prodromouf
profanis
profanis
Target Audience
Ready to modernize workflows and move away from "spaghetti" monoliths.
Forward-Thinking Engineers
Familiar with Core-Feature-Shared but ready for a more scalable approach.
Architecture Seekers
1+ years of professional experience with the core framework.
Experienced Angular Developers
profanis
What You DON'T Need to Know
Learn how to simplify your code and reduce boilerplate with Signal patterns.
No Mastery of RxJS Required
We will cover the mental shift from Zone.js to fine-grained reactivity.
No Prior Signals Knowledge Needed
We’ll learn the CLI and workspace structure from the ground up.
No Prior Nx Experience Required
🧑💻 Nx Foundation & Architecture
90 mins
☕ Short Break
15 mins
🧑💻 Nx Architecture & the Intelligent Graph
90 mins
🍌 Short Break
15 mins
🧑💻 Introducing Signals
90 mins
🧑🍳 Lunch Break
45 mins
🧑💻 Advanced Reactivity
90 mins
🍌 Short Break
15 mins
🙋 Wrap up and Q&A
30 mins
profanis
Building Scalable "Moduliths" with Angular
profanis
profanis
profanis
profanis
Products List
Product Details
Infinite Scroll
Search Functionality
tbd...
profanis
profanis
profanis
We aren't here to just learn a new library.
We are here to fix the way we work.
profanis
Daily frustrations we all face
Fearing circular dependencies and accidental coupling when sharing code.
The "Spaghetti" Fear
Debating folder structures because the standard CLI lacks a roadmap for scale.
Architecture Debt
Running 1,000 tests when you only touched two files.
Wasted Effort
A smart build system (like Angular CLI on steroids)
Modern Tooling
Architectural Guardrails
Smart Rebuilds ("Affected")
Computation Caching
profanis
A smart build system (like Angular CLI on steroids)
Computation Caching
profanis
nx testA smart build system (like Angular CLI on steroids)
Smart Rebuilds ("Affected")
profanis
nx build --affectedA smart build system (like Angular CLI on steroids)
Architectural Guardrails
profanis
nx lintA smart build system (like Angular CLI on steroids)
Modern Tooling
profanis
Reusing code between a Customer App and an Admin App is difficult.
Hard to Share
Compiles one project at a time
Project Centric
Features accidentally depend on each other
Tight Coupling
Dump everything into src/app
profanis
src/
└── app/
├── core/
│ ├── guards/
│ │ ├── auth.guard.ts
│ │ └── auth.guard.spec.ts
│ ├── header/
│ │ ├── header.component.ts
│ │ ├── header.component.html
│ │ └── header.component.scss
│ ├── core.service.ts
│ └── core.service.spec.ts
├── features/
│ ├── feature-1/
│ │ ├── feature-1.component.ts
│ │ ├── feature-1.component.html
│ │ ├── feature-1.routes.ts
│ │ └── feature-1.service.ts
│ └── feature-2/
│ ├── feature-2.component.ts
│ ├── feature-2.component.html
│ ├── feature-2.routes.ts
│ └── feature-2.service.ts
├── shared/
│ ├── components/
│ │ ├── button/
│ │ │ ├── button.component.ts
│ │ │ ├── button.component.html
│ │ │ └── button.component.scss
│ │ └── input/
│ │ ├── input.component.ts
│ │ ├── input.component.html
│ │ └── input.component.scss
│ ├── guards/
│ │ ├── can-leave.guard.ts
│ │ └── can-leave.guard.spec.ts
│ └── pipes/
│ ├── format-date.pipe.ts
│ └── format-date.pipe.spec.ts
├── app.component.ts
├── app.component.html
├── app.routes.ts
├── app.config.ts
└── main.tsdump everything into src/app
profanis
src/
└── app/
├── core/
│ ├── guards/
│ │ ├── auth.guard.ts
│ │ └── auth.guard.spec.ts
│ ├── header/
│ │ ├── header.component.ts
│ │ ├── header.component.html
│ │ └── header.component.scss
│ ├── core.service.ts
│ └── core.service.spec.ts
├── features/
│ ├── feature-1/
│ │ ├── components/
│ │ │ ├── component-a.ts
│ │ │ └── component-b.ts
│ │ ├── services/
│ │ │ ├── service-a.ts
│ │ │ └── service-b.ts
│ │ ├── pipes/
│ │ │ ├── pipe-a.ts
│ │ │ └── pipe-b.ts
│ │ ├── feature-1.component.ts
│ │ ├── feature-1.component.html
│ │ ├── feature-1.routes.ts
│ │ └── feature-1.service.ts
│ └── feature-2/
│ ├── feature-2.component.ts
│ ├── feature-2.component.html
│ ├── feature-2.routes.ts
│ └── feature-2.service.ts
├── app.component.ts
├── app.component.html
├── app.routes.ts
├── app.config.ts
└── main.tsdump everything into src/app
profanis
Features accidentally depend on each other
Tight Coupling
// src/app/core/logger.service.ts
import { FeatureAServiceService }
from '../features/feature-a.service'; // <-- Oops
@Injectable({ providedIn: 'root' })
export class LoggerService {
private featureService = inject(FeatureAService)
}// src/app/feature-1.service.ts
import { Feature2Service }
from '../feature-2.service'; // Dependency A -> B
@Injectable()
export class Feature1Service {
}
// src/app/feature-2.service.ts
import { Feature1Service }
from '../feature-1.service'; // Dependency B -> A
export class Feature2Service {
}profanis
Compiles one project at a time
Project Centric
profanis
Reusing code between a Customer App and an Admin App is difficult.
Hard to Share
profanis
You don't need 50 apps to use Nx
Concept
- We build a Single Application
Monolith deployment
- We structure it like Microservices
Modular development
profanis
my-workspace/
├── apps/
│ └── main-app/
│ ├── app.component.ts
│ ├── app.config.ts
│ ├── app.routes.ts
│ └── main.ts
└── libs/
├── auth/
│ └── guards/
│ ├── auth.guard.ts
│ └── auth.guard.spec.ts
├── shared/
│ ├── ui/
│ │ └── header/
│ │ ├── header.component.ts
│ │ ├── header.component.html
│ │ └── header.component.scss
│ └── data-access/
│ ├── core.service.ts
│ └── core.service.spec.ts
├── feature-1/
│ ├── components/
│ ├── services/
│ ├── pipes/
│ ├── feature-1.component.ts
│ ├── feature-1.routes.ts
│ └── index.ts
└── feature-2/
├── feature-2.component.ts
├── feature-2.routes.ts
└── index.tsYou don't need 50 apps to use Nx
Strategy
- Treat your libs folder like internal npm packages
- Strict boundaries between features
- Clear public APIs
profanis
You don't need 50 apps to use Nx
Why
Lack of organization slows down engineers
profanis
/apps
- The shell
- Entry point that wires everything up
/libs
- business logic
- components
- UI components
📂 workspace-root/
├── 📂 apps/
│ ├── 📂 customer-portal/ <-- App Shell
└── 📂 libs/
├── 📂 data-access/ <-- Shared Logic
├── 📂 ui-components/ <-- Shared UI
└── 📂 feature-orders/ <-- Feature Logicprofanis
/apps
/libs
📂 workspace-root/
├── 📂 apps/
│ ├── 📂 customer-portal/ <-- App Shell
└── 📂 libs/
├── 📂 data-access/ <-- Shared Logic
├── 📂 ui-components/ <-- Shared UI
└── 📂 feature-orders/ <-- Feature Logicprofanis
Utility
- Like Date Formatters
Data-Access
- Like our Product API Service
UI
- Like our Product Card
The structure is the solution
Feature
- Like our Catalogue Page
profanis
my-workspace/
├── apps/
│ └── main-app/
│ ├── app.component.ts
│ ├── app.config.ts
│ ├── app.routes.ts
│ └── main.ts
└── libs/
├── auth/
│ └── guards/
│ ├── auth.guard.ts
│ └── auth.guard.spec.ts
├── shared/
│ ├── ui/
│ │ └── header/
│ │ ├── header.component.ts
│ │ ├── header.component.html
│ │ └── header.component.scss
│ └── data-access/
│ ├── core.service.ts
│ └── core.service.spec.ts
├── feature-1/
│ ├── components/
│ ├── services/
│ ├── pipes/
│ ├── feature-1.component.ts
│ ├── feature-1.routes.ts
│ └── index.ts
└── feature-2/
├── feature-2.component.ts
├── feature-2.routes.ts
└── index.tsOnly Export What You Mean To Share
The Problem
- developers can import any file deep within another library
import { InternalHelper }
from 'libs/data-access/src/lib/internal-folder/helper.service'profanis
Only Export What You Mean To Share
The Solution
- Every Nx library has a barrel file, typically located at libs/my-lib/src/index.ts
- Anything you don't export from this file is considered "private" to that library.
profanis
Only Export What You Mean To Share
Example
📂 libs/data-access/
├── 📂 src/lib/
│ ├── internal-helper.service.ts <-- Private file
│ └── public-product.service.ts <-- Public file
└── index.ts <-- Public API// index.ts content:
export * from './src/lib/public-product.service';
// We do NOT export internal-helper.service.ts here!profanis
NX Tooling
Powerful CLI
# Create a feature library
npx nx g @nx/angular:library --name=feature-home
--directory=libs/feature-home
# Create a UI library
npx nx g @nx/angular:library --name=ui-header
--directory=libs/shared/ui/headerprofanis
Create a workspace
Create a Library
Generate a Component
CLI
more: https://nx.dev/docs/technologies/angular/guides/nx-and-angular
https://nx.dev/docs/reference/nx-commands
profanis
npx create-nx-workspace@latestnx generate @nx/angular:library [name]nx generate @nx/angular:component [name] --project=[project-name]NX Tooling
NX Console
profanis
profanis
profanis
profanis
profanis
- Create the Nx Workspace
- Install the Nx Console plugin in VS Code/WebStorm.
- Generate the feature-catalogue using NX CLI
- Generate the feature-home using the NX Console
- Setup the router
profanis
Create the NX Workspace
npx create-nx-workspace@latest --preset angular-monorepoHint
profanis
✔ Where would you like to create your workspace? · greenHeaven
✔ Application name · greenHeaven
✔ Which bundler would you like to use? · esbuild
✔ Default stylesheet format · scss
✔ Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? · No
✔ Which unit test runner would you like to use? · jest
✔ Test runner to use for end to end (E2E) tests · none
✔ Try the full Nx platform? · skip
✔ Which CI provider would you like to use? · skip
✔ Would you like remote caching to make your build faster? · skip
Create the NX Workspace
profanis
Install the Nx Console plugin in VS Code
profanis
Setup & Exploration
Explore the file system
and identify the:
- apps/greenHeaven/project.json
- nx.json
profanis
Create the Products-List Feature using the NX CLI
npx nx g @nx/angular:library --name=feature-catalogue
--directory=libs/feature-catalogueHint
profanis
Create the Home Feature using the NX Console
Hint
profanis
- right click on the libs directory
- click the Nx Generate (UI)
Create the Home Feature using the NX Console
profanis
- select @nx/angular - library
Create the Home Feature using the NX Console
Hint
profanis
Verify the barrel files and the project.json
- Open the index.ts of both libraries and verify the components are exported.
- Open the project.json of each library and identify the name of the library and the projectType
profanis
Create the Routing
// apps/greenHeaven/src/app/app.routes.ts
export const appRoutes: Route[] = [
{
path: '',
loadComponent: () =>
import('@green-heaven/feature-home').then((m) => m.FeatureHome),
pathMatch: 'full',
},
{
path: 'catalogue',
loadComponent: () =>
import('@green-heaven/feature-catalogue').then((m) => m.FeatureCatalogue),
},
];profanis
Create the Routing
- Update app.html to include a simple navbar (just text links for now)
- npx nx serve greenHeaven
- Navigate between "Home" and "Catalogue"
profanis
Create the Routing
Ensure the Feature is Rendered
profanis
Types, Scopes, and the Shell Pattern
profanis
profanis
State (Signals), NGXS/NGRX, HTTP Services
Data-Access (type:data-access)
Dumb components. Pure presentation
UI (type:ui)
Pure functions, helpers, validators
Utility (type:util)
The building blocks
The "Smart" logic and routing.
Feature (type:feature)
profanis
type:feature
Rules
It’s the only library type allowed to have routes.
profanis
The role
Smart components that orchestrate the UI components.
GreenHeaven example
feature-catalogue
What it does
Orchestrates the entire page
Contains
Domain-specific logic
Page Layouts
Services / Facades
type:ui
GreenHeaven example
ui-product-card
Rules
It just receives input(s) and emits output(s)
profanis
The role
"Dumb" components focused on pure presentation.
Contains
Cards
Business UI Components
Buttons
What it does
A UI unit of the entire application
type:data-access
GreenHeaven example
catalogue-data-access
profanis
The role
Managing State and API communication.
Contains
HTTP Services
NGXS/NGRX
Angular Signals/State
What it does
Communicates with the HTTP API
Rules
It has no UI components
type:util
profanis
The role
Low-level, pure helper functions
Contains
Date formatters
Math helpers
Custom validators
GreenHeaven example
-
What it does
Increases the reusability
Rules
Has zero dependencies on other library types.
Q: "Where do I put this file?"
Which project owns this responsibility?
A: Yes -> create a feature library
A: Yes -> create a UI library
A: Yes -> create a data-access library
Q: "I should develop a new pageable component"
Q: "I should develop a reusable UI element
Q: "I should develop a state management slice
src/
└── app/
├── core/
│ ├── guards/
│ │ ├── auth.guard.ts
│ │ └── auth.guard.spec.ts
│ ├── header/
│ │ ├── header.component.ts
│ │ ├── header.component.html
│ │ └── header.component.scss
│ ├── core.service.ts
│ └── core.service.spec.ts
├── features/
│ ├── feature-1/
│ │ ├── components/
│ │ │ ├── component-a.ts
│ │ │ └── component-b.ts
│ │ ├── services/
│ │ │ ├── service-a.ts
│ │ │ └── service-b.ts
│ │ ├── pipes/
│ │ │ ├── pipe-a.ts
│ │ │ └── pipe-b.ts
│ │ ├── feature-1.component.ts
│ │ ├── feature-1.component.html
│ │ ├── feature-1.routes.ts
│ │ └── feature-1.service.ts
│ └── feature-2/
│ ├── feature-2.component.ts
│ ├── feature-2.component.html
│ ├── feature-2.routes.ts
│ └── feature-2.service.ts
├── app.component.ts
├── app.component.html
├── app.routes.ts
├── app.config.ts
└── main.tsQ: "Do I need a library even for a component?"
A: No -> the component is an implementation detail
The feature library is allowed to be "fat" internally
Use the index.ts to hide the complexity
Organizing by Domain
As the app grows, a flat libs/ folder with 100+ libraries becomes hard to find code and hard to see ownership
The problem
Group libraries by Scope (Domain)
The solution
profanis
App
Util
Data
UI
Feat
UI
UI
Feat
Data
UI
UI
Util
UI
Util
Feat
Feat
Feat
Feat
Feat
profanis
App
Util
Data
UI
Feat
UI
UI
Feat
Data
UI
UI
Util
UI
Util
Feat
Feat
Feat
Feat
Feat
Scope A
Scope B
Scope C
profanis
App
Util
Data
UI
Feat
UI
UI
Feat
Data
UI
UI
Util
UI
Util
Feat
Feat
Feat
Feat
Feat
Scope A
Scope B
Scope C
profanis
The Entry Point for a domain
If the App knows about "List" and "Details", the App is too smart
The problem
The App knows the Shell.
The Shell sets up the child routes for the domain
The solution
profanis
App
Util
Data
UI
Feat
UI
UI
Feat
Data
UI
UI
Util
UI
Util
Feat
Feat
Feat
Feat
Feat
Scope A
Scope B
Scope C
profanis
App
Util
Data
UI
Feat
UI
UI
Feat
Data
UI
UI
Util
UI
Util
Feat
Feat
Feat
Feat
Feat
Scope A
Scope B
Scope C
Shell
Shell
Shell
profanis
App
Util
Data
UI
Feat
UI
UI
Feat
Data
UI
UI
Util
UI
Util
Feat
Feat
Feat
Feat
Feat
Scope A
Scope B
Scope C
Shell
Shell
Shell
profanis
App
Util
Data
UI
Feat
UI
UI
Feat
Data
UI
UI
Util
UI
Util
Feat
Feat
Feat
Feat
Feat
Scope A
Scope B
Scope C
Shell
Shell
Shell
profanis
It orchestrates how the work is started and displayed.
A specific sidebar, header, or footer that only appears within this feature module
Wrapper Layouts
Provides NGXS/NgRx state slices for the entire domain.
State Bootstrapping
Handle the auth guards and route resolvers
Guards and Resolvers
Defines the top-level routes that the main app loads.
Routing & Lazy Loading
profanis
App
Util
Data
UI
Feat
UI
UI
Feat
Data
UI
UI
Util
UI
Util
Feat
Feat
Feat
Feat
Feat
Scope A
Scope B
Scope C
Shell
Shell
Shell
profanis
The Entry Point for a domain
// src/app/routes.ts
export const appRoutes: Route[] = [
{ path: '', pathMatch: 'full', redirectTo: 'home' },
{
path: 'catalogue',
loadComponent: () =>
import('@workshop/catalogue-feature-catalogue-list').then(
(m) => m.CatalogueComponent
),
},
{
path: 'catalogue/:id',
loadComponent: () =>
import('@workshop/catalogue-feature-catalogue-details').then(
(m) => m.CatalogueDetailsComponent
),
},
{ path: '**', redirectTo: '' },
];profanis
The Entry Point for a domain
// src/app/routes.ts
export const appRoutes: Route[] = [
{ path: '', pathMatch: 'full', redirectTo: 'home' },
{
path: 'catalogue',
loadChildren: () =>
import('@workshop/catalogue-feature-shell').then(
(m) => m.catalogueRoutes
),
},
{ path: '**', redirectTo: '' },
];profanis
App
Scope A
Scope B
Scope C
Feat
UI
Data
Util
UI
Feat
Util
Util
Feat
UI
Data
profanis
App
UI
Data
Util
Feat
Feat
UI
Util
Util
UI
Data
Feat
Scope A
Scope B
Scope C
profanis
How does Nx know that feature-list is a "Feature"? We tell it.
type: segregates the technical responsibility
scope: segregates the functional responsibility
profanis
How does Nx know that feature-list is a "Feature"? We tell it.
// project.json
{
"name": "catalogue-feature-catalogue-details",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/catalogue/feature-catalogue-details/src",
"prefix": "lib",
"projectType": "library",
"tags": ["scope:catalogue", "type:feature"],
"targets": {
"test": { ... },
"lint": { ... }
}
}
profanis
Scope A
Util
Feat
UI
Data
How does Nx know that feature-list is a "Feature"? We tell it.
profanis
How does Nx know that feature-list is a "Feature"? We tell it.
// eslint.config.mjs
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": [
"type:ui",
"type:util",
"type:feature",
"type:data-access"
]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": [
"type:util",
"type:ui",
]
}profanis
How does Nx know that feature-list is a "Feature"? We tell it.
profanis
scope: catalogue
scope: shared
UI
Data
Util
How does Nx know that feature-list is a "Feature"? We tell it.
Feat
Util
Util
UI
Util
Util
UI
profanis
Organizing by Domain
Within a single domain, we apply our Taxonomy to keep the internal structure clean
libs/
└── catalogue/ <-- The Domain Boundary
├── data-access/ 🧠 State & API
├── feature-catalogue-details/ 📄 Catalogue Details Page
├── feature-catalogue-list/ 📄 Catalogue List Page
├── feature-shell/ 🐚 Entry Point & Internal Routing
└── types/ 🐚 Domain specific types
profanis
// project.json
{
"name": "catalogue-feature-catalogue-details",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/catalogue/feature-catalogue-details/src",
"prefix": "lib",
"projectType": "library",
"tags": ["scope:catalogue", "type:feature"],
"targets": {
"test": { ... },
"lint": { ... }
}
}
Naming consistency is key for tools and humans
profanis
Naming consistency is key for tools and humans
Data
Util
UI
scope: users
Data
Util
UI
scope: catalogue
product-list
product-list
profanis
Naming consistency is key for tools and humans
Data
Util
UI
scope: users
Data
Util
UI
scope: catalogue
product-list
product-list
product-list
product-list
profanis
scope-type-identifier*
catalogue
shared
auth
feature
ui
data-access
catalogue-list
header
users*
profanis
scope-type-identifier*
catalogue-feature-catalogue-list
shared-ui-header
auth-data-access-users
profanis
scope-type-identifier*
catalogue-feature-catalogue-list
shared-ui-header
auth-data-access-users
// project.json
{
"name": "catalogue-feature-catalogue-list",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/catalogue/feature-catalogue-list/src",
"prefix": "lib",
"projectType": "library",
....
}
profanis
scope-type-identifier*
catalogue-feature-catalogue-list
shared-ui-header
auth-data-access-users
@org/catalogue-feature-catalogue-list
@org/shared-ui-header
@org/auth-data-access-users
profanis
scope-type-identifier*
catalogue-feature-catalogue-list
shared-ui-header
auth-data-access-users
@org/catalogue-feature-catalogue-list
@org/shared-ui-header
@org/auth-data-access-users
// tsconfig.base.json
{
"compilerOptions": {
"paths": {
"@org/catalogue-feature-catalogue-list": [
"libs/catalogue/feature-catalogue-list/src/index.ts"
],
"@org/shared-ui-header": [
"libs/shared/ui/ui-header/src/index.ts"
],
"@org/auth-data-access-users": [
"libs/auth/data-access/src/index.ts"
]
}
}
}
profanis
Visualize your architecture
Defines the top-level routes that the main app loads.
nx graphVisualize only the affected by your changes projects
nx graph --affectedVisualize only the affected by your changes projects - CI
nx affected -t build --base=origin/main~1 --head=origin/mainprofanis
Visualize if your architecture is messy > nx graph
profanis
Visualize if your architecture is messy > nx graph --affected
profanis
Visualize if your architecture is messy > detect an error
profanis
- Create a "shell" library for the Catalogue
- Create a "shell" library for the Home
- Create a catalogue data-access library
- Create a ui product-card library
- Identify and solve the problem with the product.ts
profanis
Start Fresh
Preparation
git clone --branch lab/one https://github.com/profanis/nx-signals-workshop.git
cd nx-signals-workshop/
npm iLet me guide you through...
profanis
Create a "shell" library for the Catalogue 1/3
Hint
npx nx g @nx/angular:library --directory=libs/catalogue/feature-shell --name=catalogue-feature-shell- Use the Nx CLI
profanis
- Delete the auto-generated component
- Create the lib-routes.ts file under lib dir
Create a "shell" library for the Catalogue 2/3
Hint
// lib.routes.ts
export const catalogueRoutes: Route[] = [
{
path: '',
// ??
},
{
path: ':id',
// ??
},
];profanis
Create a "shell" library for the Catalogue 3/3
Hint
- Update the app/app.routes.ts
// app/app.routes.ts
{
path: 'catalogue',
loadChildren: () =>
import('PATH TO FEATURE SHELL').then(
(m) => m.ROUTES
),
},profanis
(note: locate the path of the library in the tsconfig.base.json)
Create a "shell" library for the Home
Hint
- Update the app/app.routes.ts
{
path: 'home',
loadChildren: () =>
import('PATH TO FEATURE SHELL').then(
(m) => m.ROUTES
),
}- Use the NX console and give the correct project name
profanis
Create a catalogue data-access library
- Move the file from
libs/catalogue/feature-catalogue-list/src/lib/services/products.api.ts
to data-access library
profanis
Create a ui library for the product-card
- Move the file from
libs/catalogue/feature-catalogue-list/src/lib/components/product-card.component.ts
to ui-product-card
profanis
lint all - problem
npx nx run-many -t lintRun the command
it will throw...
...
Linting "home-feature-home"...
/libs/home/feature-home/src/lib/home-page.component.ts
2:1 error A project tagged with "type:feature" can only depend on libs tagged with
"type:ui", "type:util", "type:model", "type:feature", "type:data-access"
@nx/enforce-module-boundariesprofanis
lint all - solution
Identify the project.json of the libraries:
Add the tag -> "type:ui"
npx nx run-many -t lintRun the command again:
Identify the module boundaries in the eslint.config.mjs
profanis
Identify and solve the problem with the product card
Hint
nx graphprofanis
Identify and solve the problem with the product card -> solution
Hint
nx graphprofanis
Βridge the Structure (Nx) and State (Signals)
profanis
profanis
- smart variables that notify anyone who's interested when their value changes.
What are Signals?
- Traditional change detection checks everything. Signals allow tracks specific values
profanis
- Traditional Change Detection can be slow on complex applications
Why Signals?
- zone.js is great but triggers the CD multiple times
profanis
Zone.js
User Interaction
dom event (click)
zone.js
(Angular Zone)
Change Detection
UI Update
profanis
profanis
Angular checks the entire component tree when the micro-task queue is empty
profanis
Default + Observable
OnPush + Observable
OnPush + Signals
profanis
OnPush + Observable
OnPush + Signals
profanis
OnPush + Signals
profanis
profanis
Observers
Subject
Consumers
Producers
profanis
Template
counter
Consumers
Producers
profanis
counter = signal<number>(0);
Producer
profanis
counter = signal<number>(0);
Returns a WritableSignal
Producer
profanis
counter = signal<number>(0);
Define the type
Producer
profanis
counter = signal<number>(0);
Default value
Producer
profanis
<div>
{{ counter() }}
</div>Consumer
Template Context
Consumer
profanis
const evenOrOdd =
computed(() => counter() % 2 === 0 ? 'even' : 'odd');
Consumer & Producer
Producer
Consumer
profanis
computed
profanis
const derivedState =
computed(() => mySignalArray().length);- The derivedState is getting updated when the source signal has a change
* source signal = mySignalArray()
profanis
effect
profanis
constructor() {
effect(() => {
console.log(mySignalArray().length)
})
}- The effect runs synchronously
- The effect registers the referenced signals as dependencies.
- Logging
- DOM Manipulation
- Storage handling (e.g. localStorage)
- Update other signals*
Use for
profanis
equality
profanis
mySignalArray = signal([], { equal: _.isEqual })
constructor() {
effect(() => {
console.log(mySignalArray().length)
})
}- by default signals use the Object.is() comparison
- optionally provide an equality function
profanis
signal
inputs
profanis
@Component({...})
export class MyComponent {
@Input() isChecked = false;
}profanis
@Component({...})
export class MyComponent {
isChecked = input(false);
}read-only signal
profanis
export interface UserModel {
name: string;
age: number;
/*Social*/
address: string;
twitter: string;
linkedin: string;
github: string;
instagram: string;
facebook: string;
website: string;
email: string;
}profanis
@Component({...})
export class MyComponent implements OnChanges {
@Input({ required: true }) user!: UserModel;
userSocials: string[] = [];
ngOnChanges(changes: SimpleChanges): void {
if (changes.user) {
const { name, age, ...userSocials } = this.user;
this.userSocials = Object.values(userSocials);
}
}
}profanis
@Component({...})
export class MyComponent {
user = input.required<UserModel>();
userSocials = computed(() => {
const { name, age, ...userSocials } = this.user();
return Object.values(userSocials);
});
}profanis
model input
profanis
@Component({...})
export class ChildComponent {
@Input({ required: true }) name!: string;
@Output() nameChange = new EventEmitter<string>();
}profanis
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `
<app-child [name]="username"
(nameChange)="changeHandler($event)" />
`,
})
export class ParentComponent {
username = 'profanis';
}profanis
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `
<app-child [name]="username"
(nameChange)="changeHandler($event)" />
`,
})
export class ParentComponent {
username = 'profanis';
}profanis
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: ` <app-child [(name)]="username" /> `,
})
export class ParentComponent {
username = 'profanis';
}profanis
@Component({...})
export class ChildComponent {
// @Input({ required: true }) name!: string;
// @Output() nameChange = new EventEmitter<string>();
name = model<string>();
}profanis
@Component({...})
export class ChildComponent {
// @Input({ required: true }) name!: string;
// @Output() nameChange = new EventEmitter<string>();
name = model.required<string>();
}profanis
@Component({...})
export class ChildComponent {
name = model<string>(); // writable signal
addTitle() {
this.name.update((name) => `Mr. ${name}`);
}
}profanis
@Component({...})
export class ChildComponent {
name = model<string>(); // writable signal
titleExists = computed(() => this.name().startsWith('Mr.'));
}profanis
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: ` <app-child [(name)]="username" /> `,
})
export class ParentComponent {
username = 'profanis';
}profanis
new output
profanis
@Component({...})
export class ChildComponent {
name = input.required<string>();
@Output() nameChange = new EventEmitter<string>();
}profanis
@Component({...})
export class ChildComponent {
name = input.required<string>();
nameChange = output<string>()
}profanis
@Component({...})
export class ChildComponent {
@Output() formIsValid = this.form.statusChanges.pipe(
map((status) => status === 'VALID'),
);
}profanis
import { outputFromObservable } from '@angular/core/rxjs-interop';
@Component({...})
export class ChildComponent {
formIsValid = outputFromObservable(
this.form.statusChanges.pipe(
map((status) => status === 'VALID'))
);
}profanis
import { outputToObservable } from '@angular/core/rxjs-interop';
@Component({...})
export class ChildComponent {
name = input.required<string>();
nameChange = output<string>()
nameChange$ = outputToObservable(nameChange)
}profanis
profanis
view queries
@Component({
selector: 'app-parent',
template: ` <div #child>Child wrapper</div> `,
})
export class ParentComponent {
child = viewChild('child', { read: ElementRef<HTMLElement> });
constructor() {
effect(() => {
const child = this.child();
if (child) {
console.log('Child element:', child.nativeElement);
}
});
}
}profanis
@Component({
selector: 'app-parent',
template: ` <div #child>Child wrapper</div> `,
})
export class ParentComponent {
child = viewChild.required('child', { read:
ElementRef<HTMLElement> });
constructor() {
effect(() => {
const child = this.child();
console.log('Child element:', child.nativeElement);
});
}
}profanis
<app-parent>
<app-child></app-child>
</app-parent>profanis
@Component({
selector: 'app-parent',
imports: [ChildComponent],
})
export class ParentComponent {
child = contentChild(ChildComponent);
constructor() {
effect(() => {
const childComponent = this.child();
});
}
}<app-parent>
<app-child></app-child>
</app-parent>profanis
@Component({
selector: 'app-parent',
imports: [ChildComponent],
})
export class ParentComponent {
child = contentChild.required(ChildComponent);
constructor() {
effect(() => {
const childComponent = this.child();
});
}
}@Component({ })
export class ChildComponent {
buttonClick = output<void>();
onButtonClick() {
this.buttonClick.emit();
}
}
profanis
export class ParentComponent {
child = contentChild(ChildComponent);
constructor() {
effect(() => {
const childComponent = this.child();
if (childComponent) {
const buttonClick$ = outputToObservable(childComponent.buttonClick);
buttonClick$.subscribe(() => {
console.log('Child button was clicked!');
});
}
});
}
}- Stop calling a function in the template. Use computed
- Create a filter component and use signal input/output
- Manage Favorite Products
profanis
Stash your changes
Preparation
git stash push -u -m "my changes: lab two"Checkout
git checkout lab/twoLet me guide you through...
profanis
Stop calling a function in the template. Use computed
1. Locate the product-details.component.ts
2. The getStarArray function is currently called in the template. This is inefficient. Refactor it to use a computed signal.
Create a computed signal called commentsView that derives the comments with stars from the existing data.
4. Update the template
profanis
Create a filter component and use signal input/output
1. Locate the plant-filter.component.ts
2. Define a searchTerm input (searchTerm = input<string>())
3. Define an output for when the user types.
4. Update the catalogue.component.html and replace the plain input with this component
profanis
Manage Favorite Products 1/2
1. Locate the favorites.state.ts
2. Define a private writable signal to hold the list of favorite product IDs
3. Expose a computed signal count that returns the number of favorites
4. Expose a method isFavorite(id: string) that returns boolean for a specific ID
5. Implement toggleFavorite(id: string) to update the state
profanis
Manage Favorite Products 2/2
6. Inject the FavoritesState in the catalogue.component.ts and create a computed property that returns the products and the correct isFavorite boolean value
7. Inject the FavoritesState in the app.ts and use the favoriteCount as badge
profanis
Resources, I/O & Forms
profanis
profanis
data fetching
profanis
profanis
signal<string>
profanis
signal<string>
profanis
signal<string>
cancel
cancel
profanis
How about effect?
profanis
time - 0
time - 1
request
request
100ms
500ms
profanis
time - 0
time - 1
request
request
100ms
500ms
profanis
time - 0
time - 1
request
request
100ms
500ms
profanis
time - 0
time - 1
request
request
100ms
500ms
profanis
Sync
Async
At some point we will have the data
profanis
At some point we will have the data
We will always have data
profanis
http
isLoading
error
data
profanis
http
isLoading()
error()
data()
profanis
httpResource
profanis
httpResource makes a reactive HTTP request and exposes the request status and response value
profanis
httpResource(
?,
?
)profanis
httpResource(
string | object | function ,
?
)profanis
httpResource(
string | object | function ,
options
)profanis
// String
httpResource(`https://api.com/${signalValue()}`)dependency
profanis
// Object
httpResource(
{
url: `https://api.com/${signalValue()}`,
method: 'GET',
params: { type: `${queryParamSignalValue()}` }
}
)dependency
profanis
// Object
httpResource(
{
url: `https://api.com/${signalValue()}`,
method: 'GET',
params: { type: `${queryParamSignalValue()}` }
}
)dependency
profanis
// Object
httpResource(
{
url: `https://api.com/${signalValue()}`,
method: 'GET',
params: { type: `${queryParamSignalValue()}` }
}
)Http verb
profanis
// Function
httpResource(() => `https://api.com/${signalValue()}`)profanis
// Function
httpResource(() =>
signalValue() ?
`https://api.com/${signalValue()}` :
undefined
)profanis
// String with Options
httpResource(`https://api.com/${signalValue()}`, {
defaultValue: {},
parse: (response) => zodSchema.parse(response),
})profanis
// String
resource = httpResource(`https://api.com/${signalValue()}`)@if (resource.isLoading()) {
<div>Loading...</div>
}
@if (resource.error()) {
<div>Oops...</div>
}
@if (resource.value()) {
<div>{{ resource.value() }}</div>
}profanis
// String
resource = httpResource(`https://api.com/${signalValue()}`)derivedState = computed(
() => resource.value().map(it => it.title)
)profanis
resource = rxResource({
params: () => ({
paramName: this.paramNameAsSignal(),
}),
stream: ({ params }) => this.serviceApi.get(params.paramName),
});Dependency
Service to call
profanis
linkedSignal
profanis
listOfItems = signal(['item1', 'item2', 'item3']);
countOfItems = linkedSignal(() => this.listOfItems().length);
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3']);
countOfItems = computed(() => this.listOfItems().length);
countOfItems.set(0)
profanis
listOfItems = signal(['item1', 'item2', 'item3']);
countOfItems = linkedSignal(() => this.listOfItems().length);
countOfItems.set(0)
listOfItems = signal(['item1', 'item2', 'item3']);
countOfItems = linkedSignal({
source: this.listOfItems,
computation: (items) => items.length,
});
countOfItems.set(0)
profanis
listOfItems = signal(['item1', 'item2', 'item3']);
countOfItems = linkedSignal({
source: this.listOfItems,
computation: (items) => items.length, // 3
});
countOfItems.set(0)
profanis
listOfItems = signal(['item1', 'item2', 'item3', 'item4']);
countOfItems = linkedSignal({
source: this.listOfItems,
computation: (items) => items.length, // 4
});
countOfItems.set(0)
profanis
profanis
profanis
profanis
listOfItems = signal([
{ name: 'item 1' },
{ name: 'item 2' },
{ name: 'item 3' }
]);
// keeps the selected item
selectedItem = listOfItems[0];profanis
listOfItems = signal([
{ name: 'item 1' },
{ name: 'item 2' },
{ name: 'item 3' }
]);
// an HTTP call is happening
http.get().subscribe(data =>
this.listOfItems.set([...data])
)profanis
profanis
listOfItems = signal([
{ name: 'item 1' },
{ name: 'item 2' },
{ name: 'item 3' }
]);
selectedItem = signal<Item | null>(null);profanis
listOfItems = signal([
{ name: 'item 1' },
{ name: 'item 2' },
{ name: 'item 3' }
]);
selectedItem = linkedSignal({
source: this.listOfItems,
computation: (items, previous) =>
items.find((item) => item.name === previous?.value.name),
});profanis
profanis
const counter = signal(0);profanis
const counter = signal(0);profanis
<div> {{ counter() }} </div>profanis
<div> {{ counter() }} </div>profanis
<div> {{ counter() }} </div>profanis
<div> {{ counter() }} </div>const evenOrOdd =
computed(() => counter() % 2 === 0 ? 'even' : 'odd');profanis
<div> {{ counter() }} </div>const evenOrOdd =
computed(() => counter() % 2 === 0 ? 'even' : 'odd');profanis
<div> {{ counter() }} </div>const evenOrOdd =
computed(() => counter() % 2 === 0 ? 'even' : 'odd');profanis
"Since Angular knows how the data flows, can have a more fine-grained change detection"
profanis
profanis
const isValid = signal(true);
const username = signal('profanis');
effect(() => {
if (isValid() === true) {
console.log(username());
}
});
// Update signal values
isValid.set(false);
username.set('profanis2');profanis
effect(() => {
if (isValid() === true) {
console.log(username());
}
});
// Update signal values
isValid.set(false);
username.set('profanis2');Consumers
Producers
isValid
effect
username
Push (notification)
profanis
effect(() => {
if (isValid() === true) {
console.log(username());
}
});
// Update signal values
isValid.set(false);
username.set('profanis2');Consumers
Producers
isValid
username
Push (notification)
effect
profanis
effect(() => {
if (isValid() === true) {
console.log(username());
}
});
// Update signal values
isValid.set(false);
username.set('profanis2');Consumers
Producers
isValid
username
Pull (value)
effect
profanis
effect(() => {
console.log(`${isValid()} - ${username()}`);
});
isValid.set(false);
username.set('profanis2');Consumers
Producers
isValid
username
Push (notification)
effect
profanis
Consumers
Producers
isValid
username
Push (notification)
effect(() => {
console.log(`${isValid()} - ${username()}`);
});
isValid.set(false);
username.set('profanis2');effect
profanis
Consumers
Producers
isValid
username
Pull (value)
Pull (value)
effect(() => {
console.log(`${isValid()} - ${username()}`);
});
isValid.set(false);
username.set('profanis2');effect
profanis
effect
isValid
username
profanis
Fetch data using the rxResource
- Fetch data using rxResource
- Apply a pagination
profanis
Stash your changes
Preparation
git stash push -u -m "my changes: lab three"Checkout
git checkout lab/threeLet me guide you through...
profanis
Fetch data using rxResource
- In the catalogue.state.ts use an rxResource to get products on every page change
(get rid of the mockProducts)
profanis
Filter data using computed
- In the catalogue.state.ts convert the products to a computed property and use the searchTerm to filter the initial products
profanis
Apply a pagination
- In the catalogue.component.html invoke the onLoadMore
- Use a LinkedSignal
profanis
By Fanis Prodromou
Angular Signals: A Look Under the Hood and Beyond
I am a Senior Software Engineer with a passion for Front End development with Angular. I have developed vast experience in code quality, application architecture, and application performance.