1. Before you begin
Angular Signals introduce three reactive primitives to the Angular you know and love, simplifying your development and helping you build faster apps by default.
What you'll build
- You learn about the three reactive primitives introduced with Angular Signals:
signal()
,computed()
, andeffect()
. - Use Angular Signals to power an Angular Cipher game. Ciphers are systems for encrypting and decrypting data. In this game, users can decode a secret message by dragging and dropping clues to solve a cipher, customize the message, and share the URL to send secret messages to friends.
Prerequisites
- Knowledge of Angular and Typescript
- Recommended: Watch Rethinking Reactivity with Signals to learn about the Angular Signals library
2. Get the code
Everything you need for this project is in a Stackblitz. Stackblitz is the recommended method for working through this codelab. As an alternative, clone the code and open it in your favorite dev environment.
Open Stackblitz and run the app
To get started, open the Stackblitz link in your favorite web browser:
- Open a new browser tab and go to https://stackblitz.com/edit/io-signals-codelab-starter?file=src%2Fcipher%2Fservice.cipher.ts,src%2Fsecret-message%2Fservice.message.ts&service.massage.ts
- Fork the Stackblitz to create your own editable workspace. Stackblitz should automatically run the app, and you're ready to go!
Alternative: Clone the repository and serve the app
Using VSCode or a local IDE is an alternative method for working through this codelab:
- Open a new browser tab and go to https://github.com/angular/codelabs/tree/signals-get-started.
- Fork and clone the repository, and use the
cd codelabs/
command to move into the repository. - Check out the starter code branch with the
git checkout signals-get-started
command. - Open the code in VSCode or your preferred IDE.
- To install the dependencies required to run the server, use the
npm install
command. - To run the server, use the
ng serve
command. - Open a browser tab to http://localhost:4200.
3. Establish a baseline
Your starting point is an Angular Cipher game, but it's not working yet. Angular Signals will power the game's functionality.
To get started, walk through the finished version of what you'll be building: Angular Signals Cypher.
- View the coded message on the screen.
- Drag and drop a letter button in the keypad to work toward solving the cipher and decoding the secret message.
- On success, see how the message updates to decode more of the secret message.
- Click Customize to change the Sender and Message, and then click Create & Copy URL to see the values on the screen and the URL change.
- Bonus: Copy and paste the URL to a new tab, or share with a friend, and see how the sender and message are stored in the URL.
4. Define your first signal()
A signal is a value that can tell Angular when it changes. Some signals can be changed directly, while others calculate their values from the values of other signals. Together, signals create a directed graph of dependencies that models how data flows in your app.
Angular can use the notifications from signals to know which components need to be change-detected or to execute effect functions that you define.
Convert superSecretMessage
to a signal()
superSecretMessage
is a value in MessageService
that defines the secret message the player decodes. Currently, the value does not notify the app of changes, so the Customize button is broken. You can solve this with a signal.
By making superSecretMessage
a signal, you can notify parts of the app that depend on knowing when the message has changed. When you customize the message in a dialog, you'll set the signal to update the rest of the app with the new message.
To define your first signal, perform the following steps under the TODO(1): Define your first signal()
comment in each file:
- In the
service.message.ts
file, use the Signals library to makesuperSecretMessage
reactive:
src/app/secret-message/service.message.ts
superSecretMessage = signal(
'Angular Signals are in developer preview in v16 today!'
);
This automatically prompts you to import signal
from @angular/core
. If you refresh the page, you'll likely run into errors where you previously referred to superSecretMessage
. This is because you've changed the type of superSecretMessage
from string
to SettableSignal<string>
. You can fix this by changing all references of superSecretMessage
to use the Signals API. Wherever you read the value, call the Signal getter superSecretMessage()
. And wherever you write the value, use the .set
API on SettableSignal
to set the new value for the message.
- In the
secret-message.ts
andservice.message.ts
files, update all references ofsuperSecretMessage
tosuperSecretMessage()
:
src/app/secret-message/secret-message.ts
// Before
this.messages.superSecretMessage
this.messages.superSecretMessage = message;
// After
this.messages.superSecretMessage()
this.messages.superSecretMessage.set(message);
src/app/secret-message/service.message.ts
// Before
this.superSecretMessage
// After
this.superSecretMessage()
Explore the two other signals
- Notice that you have two other signals in your app:
src/app/cipher/service.cipher.ts
cipher = signal(this.createNewCipherKey());
decodedCipher = signal<CipherKey[]>([]);
The CipherService
defines a cipher
signal, a randomly generated mapping of key-value pairs of one letter of the alphabet to a new cipher
letter. You use this to scramble the message and determine if the player finds a successful match on the keyboard.
You also have a decodedCipher
signal of the successfully decoded key-value pairs that you'll add to as the player solves the cipher.
A unique and powerful attribute of Angular's Signals library design is that you can introduce reactivity everywhere. You defined signals once in the app's services, and you can use them in a template, components, pipes, other services, or anywhere you can write application code. They're not limited or bound to a component scope.
Verify changes
- You have one more step to perform before the app works. For now, try adding a
console.log()
in different parts of your app to see how your newsuperSecretMessage
is being set.
5. Define your first computed()
In many situations you might find yourself deriving state from existing values. It's better to have the derived state update when the dependent value changes.
With computed()
, you can declaratively express a signal that derives its value from other signals.
Convert solvedMessage
to a computed()
solvedMessage
translates the secretMessage
value from encoded to decoded using the decodedCipher
signal.
This is extra cool because you can see you're deriving a computed based on another computed, so any time a signal within that mapped reactive context changes, the dependencies are notified.
Currently, the solvedMessage
isn't updated when you change the secretMessage
, decodedCipher
, or superSecretMessage
. So you're not seeing updates to the screen when the player solves the cipher.
By making solvedMessage
a computed, you create a reactive context so that when you update the message or solve the cipher, you can derive state update from the tracked dependencies.
To convert solvedMessage
to a computed()
, perform the following steps under the TODO(2): Define your first computed()
comment in each file:
- In the
service.message.ts
file, use the Signals library to makesolvedMessage
reactive:
src/app/secret-message/service.message.ts
solvedMessage = computed(() =>
this.translateMessage(
this.secretMessage(),
this.cipher.decodedCipher()
)
);
This automatically prompts you to import computed
from @angular/core
. If you refresh the page, you'll likely run into errors where you previously referred to solvedMessage
. This is because you've changed the type of superSecretMessage
from string
to Signal<string>
, a function. You can fix this by changing all references of solvedMessage
to solvedMessage()
.
- In the
secret-message.ts
file, update all references ofsolvedMessage
tosolvedMessage()
:
src/app/secret-message/secret-message.ts
// Before
<span *ngFor="let char of this.messages.solvedMessage.split(''); index as i;" [class.unsolved]="this.messages.solvedMessage[i] !== this.messages.superSecretMessage()[i]" >{{ char }}</span>
// After
<span *ngFor="let char of this.messages.solvedMessage().split(''); index as i;" [class.unsolved]="this.messages.solvedMessage()[i] !== this.messages.superSecretMessage()[i]" >{{ char }}</span>
Note that unlike superSecretMessage
, solvedMessage
is not a SettableSignal
—you can't change its value directly. Instead, its value is kept up to date whenever either of its dependency signals (secretMessage
and decodedCipher
) are updated.
Explore the two other computed()
functions
- Notice that you have two other computed values in your app:
src/app/secret-message/service.message.ts
secretMessage = computed(() =>
this.translateMessage(
this.superSecretMessage(),
this.cipher.cipher()
)
);
src/app/cipher/service.cipher.ts
unsolvedAlphabet = computed(() =>
ALPHABET.filter(
(letter) => !this.decodedCipher().find((guess) => guess.value === letter)
)
);
The MessageService
defines a secretMessage
computed, the superSecretMessage
encoded by the cipher
that players work to solve.
The CipherService
defines an unsolvedAlphabet
computed, a list of all of the letters the player has not solved, which is derived from the list of solved cipher keys in decodedCipher
.
Verify changes
Now that superSecretMessage
is a signal and solvedMessage
is a computed, the app should work! Test out the game's functionalities:
- Drag and drop a
LetterGuessComponent
to aLetterKeyComponent
in yourCipherComponent
to work toward solving the cipher and decoding the secret message. - See how the
SecretMessageComponent
updates as you decode more of the secret message. - Click Customize to change the Sender and Message, and then click Create & Copy URL to see the values on the screen and the URL change.
- Bonus: Copy and paste the URL to a new tab, or share with a friend, and see how the sender and message are stored in the URL.
6. Add your first effect()
There are times when you might want something to occur when a signal has a new value. With effect()
, you can schedule and run a handler function in response to signals changing.
Add confetti when the cipher is solved
Now that the app is functional, you can add some fun by adding confetti when the cipher is solved and the secret message is decoded.
To add confetti, perform the following steps under the TODO(3): Add your first effect()
comment:
- In the
cipher.ts
file, schedule an effect to add confetti when the message is decoded:
src/app/cipher/cipher.ts
import * as confetti from 'canvas-confetti';
ngOnInit(): void {
...
effect(() => {
if (this.messages.superSecretMessage() === this.messages.solvedMessage()) {
var confettiCanvas = document.getElementById('confetti-canvas');
confetti.create()(confettiCanvas, { particleCount: 100 });
}
});
}
Notice how this effect depends on a signal and a computed value: this.messages.superSecretMessage()
and this.messages.solvedMessage()
.
Effect helps you schedule the confetti function inside a reactive context to track and reevaluate when its dependencies are updated.
Verify changes
- Try solving the cipher (hint: you can change the message to something short to test quicker!). A confetti pop will congratulate you on your first
effect()
!
7. Congratulations!
Your Angular Cipher is now ready to decode and share secret messages! Have a message for the Angular Team? Tag our social media at @Angular so we can decode it! 🎉
You now have three new reactive primitives in your Angular toolbox to simplify your development and build faster apps by default.
Learn more
Check out these codelabs:
Read these materials:
- Angular.io
- Rethinking Reactivity with Signals (Google I/O 2023)
- What's new in Angular (Google I/O 2023)