Now that we’ve taken care of the game’s logic with a service, it’s time to end our little project with a couple more components!
The Hint component
But first, let’s create a simple component to display the current hint to the user: create the file /components/hint.ts.
import { Component } from '@angular/core';
@Component({
selector: 'app-hint',
template: `
<ng-content />
`,
styles: `
:host {
background: rgba(0,0,0,.1);
color: grey;
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2rem;
padding: 2em;
text-align: center;
}
`
})
export class Hint {}
This component does nothing more than styling: it’ll place our hint on the bottom of the page via Content Projection, like this:
<app-hint>This is a hint</app-hint>
The Container
Time for our final component: the GameContainer! This one will be in charge of displaying all the words, binding the states from our service, and listening to events. Essentially, it is the main orchestrator of our game. Create the file components/game-container.ts.
@Component({
selector: 'app-game-container',
template: `
@if (gameService.game(); as game) {
<div class="game-container">
</div>
}
<app-hint>{{ gameService.currentHint() }}</app-hint>
`,
imports: [Hint],
styles: `
`
})
export class GameContainer {
gameService = inject(GameService);
ngOnInit() {
this.gameService.init();
}
}
For now, all it does is to display the current hint to the user, and it also sets up a container <div>. It is also responsible for initializing the game.
Just put it in your main component, like this:
@Component({
selector: 'app-root',
template: `
<app-game-container />
`,
imports: [GameContainer],
})
export class App {}
Now go back to the GameContainer component: let’s add some template.
From now on, we’ll be working inside the <div class="game-container"> element.
Showing the edge words
First, we’ll show the top word.
<app-word
[letters]="game.currentEdgeWords[0].split('')"
[isReadonly]="gameService.gameStatus() !== 'sorted' || gameService.gameStatus() === 'solved'"
[isFinal]="true"
[isLocked]="gameService.gameStatus() === 'error' || gameService.gameStatus() === 'unsorted'"
(lettersChange)="onLettersChange(0, $event)"
(lastKeydown)="focusWord(game.words.length + 1)"
/>
This is what we’re doing:
- We’re binding the current top word (transforming it from an array to a single string).
- We’re binding
isReadonly,isFinalandisLockedwith the appropriate expressions.
Don’t forget to import the
Wordcomponent!
We’re also listening to two events: lettersChange and lastKeydown. The first one is responsible for updating the state, the second one is needed to jump to the bottom word when the user has entered the last letter (for convenience). So, we need to create two metods.
The first one is really simple, all it has to do is to call the methods we’ve already created in our service:
onLettersChange(wordIndex: number, letters: string[]) {
if (wordIndex === 0) {
this.gameService.replaceTop(letters.join(''));
} else if (wordIndex === this.gameService.game()!.words.length + 1) {
this.gameService.replaceBottom(letters.join(''));
} else {
this.gameService.replaceWord(wordIndex - 1, letters.join(''));
}
}
The
wordIndexwe get goes from0to6, so we’re calling the appropriate method based on that.
Then, we need the ability to focus on some element in our view. To do that, we need to use viewChildren:
wordComponents = viewChildren(Word);
Import it from
@angular/core.
This is a Signal which contains all the Word components on this page. Now we can create the focusWord() handler:
focusWord(i: number) {
this.wordComponents()[i]?.focus(0);
}
Perfect! Now for the bottom word, things are mostly the same, except that we’re binding to the second edge word, and we don’t need to listen to the lastKeydown event:
<app-word
[letters]="game.currentEdgeWords[1].split('')"
[isReadonly]="gameService.gameStatus() !== 'sorted' || gameService.gameStatus() === 'solved'"
[isFinal]="true"
[isLocked]="gameService.gameStatus() === 'error' || gameService.gameStatus() === 'unsorted'"
(lettersChange)="onLettersChange(game.words.length + 1, $event)"
/>
You should now see both words, in a locked state:
The middle words
It’s time to display the middle words! In order to have the drag & drop functionality, we need the Angular CDK, so install it like this:
npm install @angular/cdk
Now, between the two words, put this template:
<div cdkDropList class="sortable-list" (cdkDropListDropped)="drop($event)">
@for (word of game.words; track i; let i = $index) {
<div cdkDrag class="word-container">
</div>
}
</div>
Here we’re displaying all of our middle words. But also:
- We’re using the
cdkDropListdirective to tell Angular that this is where we’ll put our draggable elements. - We’re using the
cdkDragdirective to the elements that we want to drag. - We’re listening to the
cdkDropListDroppedevent which tells us when an element has been dropped.
So, first of all, import the directives:
import { CdkDropList, CdkDrag } from '@angular/cdk';
// ...
imports: [Word, CdkDropList, CdkDrag, Hint, Icon],
Then, we need to implement the drop() handler. This is how it looks like:
drop(event: CdkDragDrop<string[]>) {
const words = [...this.gameService.game()!.words];
moveItemInArray(words, event.previousIndex, event.currentIndex);
this.gameService.game.update(game => ({ ...game!, words }));
}
Import
CdkDragDropandmoveItemInArrayfrom@angular/cdkand place them in theimportsarray.
Here we’re cloning the original array of words (we don’t want to mutate it) and sorting it with the helper moveItemInArray from the CDK. Then, we’re updating the state with the ordered words.
Believe it or not, the class is done! All we need to do now is to add some more elements and some styles.
Place your cursor inside the <div cdkDrag class="word-container"> element and add the following:
<app-word
[letters]="word.current.split('')"
[isReadonly]="gameService.gameStatus() !== 'error'"
(focusedChange)="gameService.focused.set($event !== null ? i : null)"
(lettersChange)="onLettersChange(i + 1, $event)"
(lastKeydown)="$last ? '' : focusWord(i + 2)"
/>
This is more or less the same thing we did for the edge words, but:
- The bindings and conditions are different.
- We’re using
i + 1on thelettersChangeevent because we need to account for the top word. - We’re using
i + 2onlastKeydownfor the same reason.
Also, we’re listening to focusedChange to know when the user has focused this word. For our state, we don’t actually need to know the index of the character (the $event), just the index of the word.
One more touch: we don’t want the user to drag the words normally, we want to make sure they drag an icon, so let’s add two icons, one per side! Place this immediately after the previous template.
<div cdkDragHandle [style.display]="gameService.gameStatus() !== 'sorted' && gameService.gameStatus() !== 'solved' ? 'block' : 'none'">
<app-icon icon="bars" />
</div>
<div cdkDragHandle [style.display]="gameService.gameStatus() !== 'sorted' && gameService.gameStatus() !== 'solved' ? 'block' : 'none'">
<app-icon icon="bars" />
</div>
Import
CdkDragHandlefrom@angular/cdkand place it in theimportsarray.
All that’s left to do is some styling! Put these styles in your component:
These are some simple styles to make it functional, feel free to tweak them!
.game-container {
max-width: 400px;
padding: 2em;
margin: 0 auto;
}
.word-container {
position: relative;
}
[cdkDragHandle] {
position: absolute;
top: 50%;
display: block;
cursor: pointer;
}
[cdkDragHandle]:nth-of-type(1) {
left: 0;
transform: translate(-50%, -50%);
}
[cdkDragHandle]:nth-of-type(2) {
right: 0;
transform: translate(50%, -50%);
}
.sortable-list {
border: 2px solid rgba(0,0,0,.05);
border-top: none;
border-bottom: none;
}
app-word {
display: block;
padding: 10px 30px;
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag {
overflow: visible;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.sortable-list.cdk-drop-list-dragging > *:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
All the classes with the cdk- prefix are automatically applied by the CDK, we just need to style them. The CDK actually clones the element when it moves, so we’re just making the previous one invisible while we drag.
And we’re done! Our game is now complete! [Link to the final code]
Where to go from here
The game is fully working, but the original one has a couple more features. Feel free to implement them as an exercise! For example:
- Request the
GameInfoobject from an actual server. - Start a timer as soon as the game is loaded, and stop it when the puzzle is solved. You could do it easily with RxJS.
- Display a button which reveals the focused word.
- Display a button which reveals only one missing letter of the focused word.
- Finally, to make it more challenging, allow the user to click on these buttons only every 10 seconds or so. Again, this should be pretty easy with RxJS!
I hope this has been a fun little project to build, as it was for me! Hope you liked it!



