This article is a guest post by Chidume Nnamdi. He wrote this blog post through the Write for the Community program. If you are passionate about everything jamstack, open-source or javascript and want to share, join the writer's guild!
This tutorial will leverage two amazing techs, Angular and Strapi, to build a movie app. The primary focus of this tutorial is to learn how we can use Strapi to build APIs and use them from an Angular app.
Read on.
In this tutorial, we will learn about Angular and Strapi.
We will learn how to:
@angular/cli
.What is Strapi?
Strapi is an open-source headless CMS (Content Management system) based on Node.js that makes the designing of APIs very fast and easy. The content of the APIs is self-hosted, customizable, and highly performant. Building backend for your projects just got very easy. With Strapi, there will be no need for endless thought on which stack to use, the database to use as it supports both RESTful and GraphQL APIs.
With Strapi, all you need to do is define your business models, then from there, create your collections and populate the content. Strapi gives you the content on endpoints following best practices.
For example, if we have a blog app and we create a blogPosts
collection in Strapi.
Strapi will provide us with four endpoints, from which we can get all blog posts, get a particular blog post, edit a specific blog post, and delete a blog post.
POST /blogPosts
This adds a blog post to the collection.
PUT /blogPosts/:id
This edits a particular blog post via the id
parameter. It will select the blog post using the id
and edit the post from the payload sent.
DELETE blogPosts/:id
This resource finds the blog post from the id
and deletes it from the collection.
GET /blogPosts/:id
This retrieves a particular blog post from the collection using the id
.
So we see how powerful Strapi is, provide your collections, and you have your endpoints. You can then consume them from mobile, web, or desktop. There is no longer a need to set up a backend, and the more amazing thing is that you can deploy the Strapi backend to the cloud and be used in the real world.
Requirements
We will need some tools and software installed before we can be able to run this project.
1- **Node.js** Strapi and Angular all run on Node.js. So we must have Node.js binaries installed on our machine. You can download it from here [Node.js download page](https://nodejs.org/).
2- **N.P.M.**: This is the official Node package manager. It comes bundled with the Node.js binaries.
3- **Yarn**: Very fast Node package manager. You can install via N.P.M.: `npm i yarn -g`.
4- **@angular/cli**: This is a CLI tool from the Angular team to create an Angular project quickly.
5- **VS Code**: This is a code editor from Microsoft. It is unarguably the most used code editor in the world. So I prefer you use this because it has enormous support and very good for modern web development. Download it from [here](https://code.visualstudio.com/download).
Scaffolding a Strapi project
We have to create our backend with Strapi. Now, we will scaffold the project using the yarn command.
Let's create a main folder that will contain booth our Strapi backend and the Angular frontend.
1mkdir movie-ng
This creates a folder movie-ng
, now we move into the folder:
1cd movie-ng
Now, we create a Strapi project:
1yarn create strapi-app movie-api --quickstart
This creates a movie-API
folder, and this is our Strapi backend. Strapi will install the dependencies and start the server for us using the command strapi develop
.
The Strapi server will be started at localhost:1337
, and the URL localhost:1337/admin
will automatically be loaded in our browser by Strapi.
Fill in your details and click on the "LET'S START" button. Strapi will create your account, and the admin U.I. will be loaded.
From this page, we create our collections.
Build the Movie collections
Our Strapi server is up and running. Now, we will build our movie collections.
Our movie model will be like this:
1movie {
2 id
3 name
4 imageUrl
5 synopsis
6 year
7 genre
8}
The above model represents the information a movie will have in our app.
id
is the unique identifier for each movie in our database. Strapi and auto-incremented generate it.name
is the title of the movie, e.g., Avengers: Endgame.imageUrl
is the image link of the movie's post cover. I did it like this so we won't have to upload an image file binary to our server to save memory space, but I will still demo how to upload files in Strapi.synopsis
summarizes a completed screenplay's core concept, major plot points, and main character arcs.year
is the year the movie was released.genre
is the thematic category of the movie. e.g., it can be a thriller, horror, comedy, etc.We have seen how a movie model will look like, and now we have to create a movie collection.
Let’s go!
On the admin UI.
Click on + Create new collection type
. A modal will pop up on the popup modal, type "movies" in the Display name input box.
These "movies" will be the name of our collection type, and the movie endpoints will be from there.
Click on the Continue
button, a modal will appear:
This will be to select field for our "movies" collection. The fields in our movie model will be a "Text" field. So choose the "Text" field on the next U.I. that shows type in "name" and click on + Add another area
.
The "Select a field for your collection type" will show for us to select another field. Select "Text," and on the next U.I., type in "imageUrl."
And click on + Add another field
. Repeat this process for the synopsis
, year
, genre
fields.
At the genre
, field click on the Finish
button. The modal will disappear, and we will see the movies displayed with the fields that we have added. On the top right, click on the "Save," this will persist our movie collections to Strapi.
Also, see that at the sidebar that a "Movies" is added there alongside the "Users."
Let's add mock movies data. To do that, click on the "Movies" on the sidebar, a page will open:
This displays the data we have on the Movies collection. Right now, there is no data yet. To add data, click on the + Add New Movies
button on the top-right page. Create an entry U.I
. will appear; you will see input boxes for all the fields in our movie model.
Add the data below in the corresponding input box:
1Name -> "Avengers: Endgame"
2ImageUrl -> "https://terrigen-cdn-dev.marvel.com/content/prod/1x/ae_digital_packshot.jpg"
3Synopsis -> "Adrift in space with no food or water, Tony Stark sends a message to Pepper Potts as his oxygen supply starts to dwindle. Meanwhile, the remaining Avengers -- Thor, Black Widow, Captain America, and Bruce Banner -- must figure out a way to bring back their vanquished allies for an epic showdown with Thanos -- the evil demigod who decimated the planet and the universe."
4Genre -> "Sci-Fi"
5Year -> 2019
After adding them:
Click on the “Save”
Now, the “Publish” button next to it becomes clickable, click on it to make the data live.
Go back to our “Movies” page, you will see our new data listed in the table.
See that it has an id
of 1.
Let’s add one more movie before we can test. Let’s add Avengers
.
Let’s go, click on the “+ Add New Movies” on the “Movies” page.
For Avengers
, add this data:
1Name -> “The Avengers”
2ImageUrl -> “https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcTp0qlAoWcOOswIkL_qpjYzJqCCDmWXiBzCXiqbE43Obo8c0Z-s"
3Genre -> Sci-Fi
4Year -> 2012
5Synopsis -> “When Thor’s evil brother, Loki (Tom Hiddleston), gains access to the unlimited power of the energy cube called the Tesseract, Nick Fury (Samuel L. Jackson), director of S.H.I.E.L.D., initiates a superhero recruitment effort to defeat the unprecedented threat to Earth. Joining Fury’s “dream team” are Iron Man (Robert Downey Jr.), Captain America (Chris Evans), the Hulk (Mark Ruffalo), Thor (Chris Hemsworth), the Black Widow (Scarlett Johansson), and Hawkeye (Jeremy Renner).”
Now, click on “Save”, and then on “Publish”. Move to the “Movies” page, see that this has an id
of 2.
We have two movie data added via Strapi admin UI.
Before we test our endpoints, let's make them accessible to the public. Click on "Settings" on the sidebar, then on the page that loads, go to the "USERS & PERMISSIONS PLUGIN" section and click on "Roles" a U.I. appears on the right. Click on "Public." A UI appears, scroll to the "Permissions" section and click on the "Select all" checkbox. This selection will allow any user to perform CRUD operations on the movie's collection.
Next, scroll up and click on the “Save” button.
Let’s test our movie endpoints via Postman.
Test the Movie endpoints using Postman
Make sure you have Postman installed. You can download it from here.
We have movies endpoints:
POST /movies/
PUT /movies/:id
GET /movies/:id
DELETE /movies/:id
GET /movies
Open your Postman, and create a collection, you can name it Movies
. Add a new request to it.
Get all movies
Let’s test the GET /movies
endpoint, enter localhost:1337/movies
in the Enter request URL
input box. Then, click on the Send
button. You will see all the movies returned.
See that Strapi adds created_at
, publishged_at
, updated_at
fields to hold when the data is created, when the data was made live and when the data was updated respectively.
Get a movie
Let’s retrieve a movie by an id. Let’s get the second movie data, enter localhost:1337/movies/2
in the input box, press "Send".
See that the movie with id 2 is returned.
Add a new movie
Let’s add a new movie using the POST /movies
endpoint.
Now, enter localhost:1337/movies
, set the HTTP method to POST, click "Body" and select "x-www-form-urlencoded".
Fill out the below fields:
1name -> Avengers: Age of Ultron
2imageUrl -> http://www.movienewsletters.net/photos/183976R1.jpg
3year -> 2015
4genre -> Sci-Fi
5synopsis -> When Tony Stark (Robert Downey Jr.) jump-starts a dormant peacekeeping program, things go awry, forcing him, Thor (Chris Hemsworth), the Incredible Hulk (Mark Ruffalo) and the rest of the Avengers to reassemble. As the fate of Earth hangs in the balance, the team is put to the ultimate test as they battle Ultron, a technological terror hell-bent on human extinction. Along the way, they encounter two mysterious and powerful newcomers, Pietro and Wanda Maximoff.
Then, click on “Send”. This will add new movie data to the Strapi.
Looking at the “Movies” page in our Strapi admin, you will see the data there.
Edit a movie
Let’s edit movie data via the /movies/:id
PUT.
Let's edit the last movie we added, we will change the name from Avengers: Age of Ultron
to The Avengers: Age of Ultron
.
Type localhost:1337/movies/4
, the 4 there is the id of the movie. Now, change the HTTP method to P.U.T. Then, in the "Body" -> "x-www-form-urlencoded", add a "name" and set the value to the "The Avengers: Age of Ultron". Click on "Send." This will edit the movie's name to "The Avengers: Age of Ultron."
Delete a movie
To delete a movie, we use the DELETE /movies/id
endpoint.
"let's delete the movie with an id of 4. Type localhost:1337/movies/4
in the input box and change the HTTP method to DELETE, then click on "Send"
This deletes the movie id 4, and only two movies will be in our database. We have successfully tested our Strapi endpoints. Let's build the front end.
Building the Angular
Make sure you have the @angular/CLI
installed on your machine. If not, pull it by running this command:
1npm i @angular/cli -g
Test the installation by checking for the version:
1$ ng --version
2Angular CLI: 11.2.7
3Node: 12.17.0
4OS: darwin x64
Scaffold Angular project
Now, we create an Angular project using the @angular/cli
tool.
1ng new movie-strapi
This will cause the ng
tool to scaffold an Angular project on the movie-strapi
folder. It will give you options and select based on how you work, but I use CSS and enable routing.
Our app will have two screens:
Movie list: It will display a list of movies.
Movie view: It will display a movie.
We will break our app into components.
Header
: This component will render the header section of our app.
Movielist
: This component will render the array of movies. It will fetch the movies and render them in a list.
Movieview
: This component will display the details of a particular movie. It will retrieve the movie id from the URL, get the movie from the Strapi backend and display it.
Moviecard
: This component renders a summary of a movie. The Movielist
component will render this component.
Editmovie
: This is a modal component. It will be used to edit a movie.
Addmovie
: This is also a modal component. It will be used to add a new movie to the app.
Create the components
Now, we will begin creating the components. We will use the ng g c
command to generate Angular components.
Now, we want the template and style of our components to be in line. It means that we don't want any .css
and .html
files created for the styles and template, respectively. Also, we don't wish to test files to be generated to skip it.
First, move into the movie-strapi
folder:
1cd movie-strapi
Open the app.component.html
file and delete the content.
header component
Let’s start with the header component. Run this below command:
1ng g c header --skip-tests -t -s
The sub-commands:
--skip-tests
: Do not create "spec.ts" test files for the new component.--inline-template (-t)
Include template inline in the component.ts file. By default, an external template file is created and referenced in the component.ts file.--inline-style (-s)
Include styles inline in the component.ts file. Only CSS styles can be included inline. By default, an external styles file is created and referenced in the component.ts file.The command will create a header
folder that will contain header.component.ts
file.
Now, open the header.component.ts
file and paste the below code:
1import { Component, OnInit } from "@angular/core";
2@Component({
3 selector: "app-header",
4 template: `
5 <section class="header">
6 <div class="headerName">MovieFlix</div>
7 </section>
8 `,
9 styles: [
10 `
11 .header {
12 height: 40px;
13 background-color: black;
14 color: white;
15 display: flex;
16 align-items: center;
17 padding: 10px;
18 font-family: sans-serif;
19 }
20 .headerName {
21 font-size: 1.8em;
22 }
23 `,
24 ],
25})
26export class HeaderComponent implements OnInit {
27 constructor() {}
28 ngOnInit(): void {}
29}
The selector app-header
is the HTML tag you will use to render this component. It just displays the title of our app MovieFlix
.
OK, now open app.component.html
and add the code:
1<app-header></app-header>
This will render our header in all pages/routes.
Now, we need to add the router-outlet></router-outlet>
tag in the app.component.html
file, because we have routing in our system. The router-outlet
tag is when the components of the page we navigate to will be rendered.
1<app-header></app-header> <router-outlet></router-outlet>
movieview component
Let’s create the movieview
component. Run the below command:
1ng g c movieview --skip-tests -t -s
This will create a movieview
folder that will contain movieview.component.ts
file.
This will render a particular movie view.
1import { HttpClient } from "@angular/common/http";
2import { ActivatedRoute, Params, Router } from "@angular/router";
3import {
4 Component,
5 OnInit,
6 TemplateRef,
7 ViewChild,
8 ViewContainerRef,
9} from "@angular/core";
10@Component({
11 selector: "app-movieview",
12 template: `
13 <div class="movieview-container">
14 <div class="movieview">
15 <div
16 class="movieview-image"
17 style="background-image: url({{ movie?.imageUrl }});"
18 ></div>
19 <div class="movieview-details">
20 <div class="movieview-name">
21 <h1>{{ movie?.name }} ({{ movie?.year }})</h1>
22 </div>
23 <div style="padding: 5px 0;">
24 <span>
25 <button
26 style="margin-left: 0;"
27 class="btn"
28 (click)="showEditMovieDialog()"
29 >
30 Edit
31 </button>
32 <button class="btn btn-danger" (click)="deleteMovie()">
33 Delete
34 </button>
35 </span>
36 </div>
37 <div style="padding: 5px 0;">
38 <span> Genre: {{ movie?.genre }}</span>
39 </div>
40 <div style="padding: 5px 0;">
41 <span>Year: {{ movie?.year }}</span>
42 </div>
43 <div class="movieview-synopsis-cnt">
44 <h2>Synopsis</h2>
45 <div class="movie-synopsis">{{ movie?.synopsis }}</div>
46 </div>
47 </div>
48 </div>
49 <ng-container #vcRef></ng-container>
50 <ng-template #modalRef>
51 <app-editmovie
52 (closeDialog)="closeDialog()"
53 [movie]="movie"
54 ></app-editmovie>
55 </ng-template>
56 </div>
57 `,
58 styles: [
59 `
60 .movieview-container {
61 display: flex;
62 justify-content: center;
63 }
64 .movieview {
65 display: flex;
66 justify-content: center;
67 padding: 15px;
68 width: 900px;
69 }
70 .movieview-image {
71 height: 500px;
72 background-repeat: no-repeat;
73 background-size: cover;
74 background-position: center;
75 margin-right: 10px;
76 padding-right: 15px;
77 flex: 5;
78 }
79 .movieview-details {
80 font-family: system-ui;
81 padding-left: 15px;
82 flex: 7;
83 }
84 .movieview-name h1 {
85 margin-top: 0;
86 border-top: 1px solid;
87 border-bottom: 1px solid;
88 padding: 10px 0;
89 }
90 .movieview-synopsis-cnt h2 {
91 border-bottom: 1px solid;
92 padding-bottom: 4px;
93 }
94 `,
95 ],
96})
97export class MovieviewComponent implements OnInit {
98 movie: any;
99 @ViewChild("modalRef") modalRef!: TemplateRef<any>;
100 @ViewChild("vcRef", { read: ViewContainerRef }) vcRef!: ViewContainerRef;
101 vRef: any = null;
102 constructor(
103 private activatedRoute: ActivatedRoute,
104 private http: HttpClient,
105 private router: Router
106 ) {}
107 ngOnInit(): void {
108 this.activatedRoute.params.subscribe((params: Params) => {
109 if (params.id) {
110 const movieId = params.id;
111 this.http
112 .get("http://localhost:1337/movies/" + movieId)
113 .subscribe((data: any) => (this.movie = data));
114 }
115 });
116 }
117 deleteMovie() {
118 if (confirm("Do you really want to delete this movie")) {
119 this.http
120 .delete("http://localhost:1337/movies/" + this.movie?.id)
121 .subscribe((data) => {
122 this.router.navigate(["/"]);
123 });
124 }
125 }
126 showEditMovieDialog() {
127 let view = this.modalRef.createEmbeddedView(null);
128 this.vcRef.insert(view);
129 }
130 closeDialog() {
131 this.vcRef.clear();
132 }
133}
We have our design in the template
property.
On render, the component uses ActivatedRoute
to get the param of the current URL and then retrieve the id
param to get the movie's id
. Then, we use the HttpClient
to call the get()
method, and we pass the URL http://localhost:1337/movies/" + this.movie?.id
to the method. Then, we subscribe to the Observable it returns to get the data. The data will have the movie data. Once we have the movie data, we set it to the movie
property.
This will cause the component to re-render and display the movie details.
See this section in the template
:
1<ng-container #vcRef></ng-container>
2<ng-template #modalRef>
3 <app-editmovie (closeDialog)="closeDialog()" [movie]="movie"></app-editmovie>
4</ng-template>
The ng-template
holds the editmovie
modal component. The ng-container
is where we will render the app-editmovie
component.
That’s why we have this:
1@ViewChild("modalRef") modalRef!: TemplateRef<any>;
2@ViewChild("vcRef", { read: ViewContainerRef }) vcRef!: ViewContainerRef;
The first code gets the instance of the ng-template
and stores it in the modelRef
variable.
The second code gets the instance of the ng-container
and stores it in the vcRef
.
The Edit
button, when clicked, calls the showEditMovieDialog
method.
1showEditMovieDialog() {
2 let view = this.modalRef.createEmbeddedView(null);
3 this.vcRef.insert(view);
4}
The first line creates a view from the modalRef
instance. Then, the second line inserts the created view in the vcRef
i.e in the ng-container
in the DOM.
This will cause the editmovie
modal to show up over all other elements on the page.
The Delete
button, when clicked, calls the system confirm dialog. If you click on "OK", the movie resource at HTTP DELETE "http://localhost:1337/movies/" + this.movie?.id
is called, which deletes the movie from the backend. Then, we use the Router
instance router
to load the movies page.
The closeDialog
method, calls the clear()
method on the vcRef
instance. The clear()
method removes the modalRef
from the ng-container
, this will cause the editmovie
modal to disappear.
movielist component
Here, we will create the movielist
component.
1ng g c movielist --skip-tests -t -s
This will create the movielist
component.
Let’s see the code for the movielist
component:
1import { HttpClient } from "@angular/common/http";
2import {
3 Component,
4 OnInit,
5 TemplateRef,
6 ViewChild,
7 ViewContainerRef,
8} from "@angular/core";
9@Component({
10 selector: "app-movielist",
11 template: `
12 <div class="movielist-cnt">
13 <div class="movielist-breadcrumb">
14 <div>
15 <h2>Trending movies</h2>
16 </div>
17 <div>
18 <button
19 class="btn"
20 style="padding-left: 15px;padding-right: 15px;font-weight: 500;"
21 (click)="showAddMovieDialog()"
22 >
23 Add Movie
24 </button>
25 </div>
26 </div>
27 <div class="movielist">
28 <app-moviecard
29 *ngFor="let movie of movies"
30 [movie]="movie"
31 ></app-moviecard>
32 </div>
33 <ng-container #vc></ng-container>
34 <ng-template #modal>
35 <app-addmovie
36 (closeDialog)="closeDialog()"
37 (refreshMovies)="fetchMovies()"
38 ></app-addmovie>
39 </ng-template>
40 </div>
41 `,
42 styles: [
43 `
44 .movielist {
45 display: flex;
46 color: grey;
47 padding: 15px;
48 flex-wrap: wrap;
49 padding-top: 0;
50 }
51 .movielist-breadcrumb {
52 font-family: system-ui;
53 display: flex;
54 justify-content: space-between;
55 padding: 32px;
56 padding-bottom: 0;
57 padding-top: 17px;
58 }
59 .movielist-breadcrumb h2 {
60 margin: 0;
61 }
62 `,
63 ],
64})
65export class MovielistComponent implements OnInit {
66 movies = [];
67 @ViewChild("modal") modal!: TemplateRef<any>;
68 @ViewChild("vc", { read: ViewContainerRef }) vc!: ViewContainerRef;
69 vRef: any = null;
70 constructor(private http: HttpClient) {}
71 ngOnInit(): void {
72 this.fetchMovies();
73 }
74 ngAfterViewInit() {
75 this.vRef = this.vc;
76 }
77 fetchMovies() {
78 this.http
79 .get("http://localhost:1337/movies")
80 .subscribe((data: any) => (this.movies = data));
81 }
82 showAddMovieDialog() {
83 let view = this.modal.createEmbeddedView(null);
84 this.vRef.insert(view);
85 }
86 closeDialog() {
87 this.vRef.clear();
88 }
89}
On the initial render of the component, it fetches all movies from the resource [http://localhost:1337/movies](http://localhost:1337/movies.)
.
It then displays the movies using the *ngFor
directive. The app-moviecard
is used to display each movie in the iteration:
1<div class="movielist">
2 <app-moviecard *ngFor="let movie of movies" [movie]="movie"></app-moviecard>
3</div>
There is an Add Movie
button when clicked it will popup the addmovie
modal component.
1<ng-container #vc></ng-container>
2<ng-template #modal>
3 <app-addmovie
4 (closeDialog)="closeDialog()"
5 (refreshMovies)="fetchMovies()"
6 ></app-addmovie>
7</ng-template>
This is the same thing as we saw in the movieview
component.
The closeDialog
is passed to the addmovie
component so we can close the component from there. The refreshMovies
is a function that will be called from the addmovie
component, this will cause the movielist
component to refresh its movies list when a new movie is added. The refreshMovies
points to fetchMovies
so the method will be called.
moviecard component
Generate the moviecard
component. Run the below command:
1ng g c moviecard --skip-tests -t -s
Let’s see the code for moviecard
component.:
1import { Component, Input, OnInit } from "@angular/core";
2@Component({
3 selector: "app-moviecard",
4 template: `
5 <div class="movie-card" routerLink="/movie/{{ movie.id }}">
6 <div
7 class="movie-card-img"
8 style="background-image: url({{ movie.imageUrl }});"
9 ></div>
10 <div class="movie-card-footer">
11 <div class="movie-card-name">
12 <h3>{{ movie.name }}</h3>
13 </div>
14 <div class="movie-card-year">
15 <span>Year</span><span>{{ movie.year }}</span>
16 </div>
17 <div class="movie-card-genre">
18 <span>Genre:</span><span>{{ movie.genre }}</span>
19 </div>
20 </div>
21 </div>
22 `,
23 styles: [
24 `
25 .movie-card {
26 border: 0px solid;
27 width: 251px;
28 height: 300px;
29 box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
30 transition: 0.3s;
31 display: flex;
32 flex-direction: column;
33 justify-content: space-between;
34 border-bottom-left-radius: 5px;
35 border-bottom-right-radius: 5px;
36 margin: 27px;
37 cursor: pointer;
38 background-color: rgba(255, 255, 255, 1);
39 }
40 .movie-card:hover {
41 box-shadow: 0 20px 36px 0 rgba(0, 0, 0, 0.2);
42 }
43 .movie-card-footer {
44 font-family: system-ui;
45 padding: 15px;
46 padding-bottom: 18px;
47 padding-top: 0;
48 }
49 .movie-card-img {
50 height: 250px;
51 background-repeat: no-repeat;
52 background-size: cover;
53 background-position: center;
54 }
55 .movie-card-footer {
56 border-radius: 5px;
57 }
58 .movie-card-name {
59 color: black;
60 font-family: system-ui;
61 }
62 .movie-card-name h3 {
63 margin-bottom: 7px;
64 margin-top: 2px;
65 }
66 .movie-card-year {
67 display: flex;
68 justify-content: space-between;
69 font-weight: 500;
70 color: darkgray;
71 padding: 4px 0;
72 }
73 .movie-card-genre {
74 display: flex;
75 justify-content: space-between;
76 font-weight: 500;
77 color: darkgray;
78 }
79 `,
80 ],
81})
82export class MoviecardComponent implements OnInit {
83 @Input() movie: any;
84 constructor() {}
85 ngOnInit(): void {}
86}
This component receives a movie from the movie
input. See that it is decorated with the @Input()
decorator, this tells Angular that this component will expect an input via the movie
channel from the parent component.
The UI just displays the movie’s image, name, genre, and year. See that we set routerLink="/movie/{{ movie.id }}"
in the div#movie-card
this will cause the card to load the movieview
page when the card is clicked. The page will display the full details of the movie.
addmovie component
To generate the addmovie
component, run the below command:
1ng g c addmovie --skip-tests -t -s
Let’s see the code:
1import { HttpClient } from "@angular/common/http";
2import { Component, EventEmitter, OnInit, Output } from "@angular/core";
3@Component({
4 selector: "app-addmovie",
5 template: `
6 <div class="modal">
7 <div class="modal-backdrop" (click)="closeModal()"></div>
8 <div class="modal-content">
9 <div class="modal-header">
10 <h3>Add Movie</h3>
11 <span style="padding: 10px;cursor: pointer;" (click)="closeModal()"
12 >X</span
13 >
14 </div>
15 <div class="modal-body content">
16 <div class="inputField">
17 <div class="label"><label>Name</label></div>
18 <div><input id="addMovieName" type="text" /></div>
19 </div>
20 <div class="inputField">
21 <div class="label"><label>ImageUrl</label></div>
22 <div><input id="addMovieImageUrl" type="text" /></div>
23 </div>
24 <div class="inputField">
25 <div class="label"><label>Synopsis</label></div>
26 <div><input id="addMovieSynopsis" type="text" /></div>
27 </div>
28 <div class="inputField">
29 <div class="label"><label>Year</label></div>
30 <div><input id="addMovieYear" type="text" /></div>
31 </div>
32 <div class="inputField">
33 <div class="label"><label>Genre</label></div>
34 <div><input id="addMovieGenre" type="text" /></div>
35 </div>
36 </div>
37 <div class="modal-footer">
38 <button (click)="closeModal()">Cancel</button>
39 <button
40 [disabled]="disable"
41 class="btn"
42 (click)="addNewMovie($event)"
43 >
44 Add
45 </button>
46 </div>
47 </div>
48 </div>
49 `,
50 styles: [
51 `
52 .label {
53 padding: 4px 0;
54 font-size: small;
55 color: rgb(51, 55, 64);
56 }
57 .content {
58 display: flex;
59 flex-wrap: wrap;
60 }
61 .inputField {
62 margin: 3px 7px;
63 flex: 1 40%;
64 }
65 `,
66 ],
67})
68export class AddmovieComponent implements OnInit {
69 @Output() closeDialog = new EventEmitter();
70 @Output() refreshMovies = new EventEmitter();
71 disable = false;
72 constructor(private http: HttpClient) {}
73 ngOnInit(): void {}
74 addNewMovie(e: Event) {
75 this.disable = true;
76 const {
77 addMovieName,
78 addMovieYear,
79 addMovieGenre,
80 addMovieImageUrl,
81 addMovieSynopsis,
82 } = window as any;
83 this.http
84 .post("http://localhost:1337/movies", {
85 name: addMovieName.value,
86 year: addMovieYear.value,
87 synopsis: addMovieSynopsis.value,
88 imageUrl: addMovieImageUrl.value,
89 genre: addMovieGenre.value,
90 })
91 .subscribe(
92 (data) => {
93 this.disable = false;
94 this.refreshMovies.emit("");
95 this.closeDialog.emit("");
96 },
97 (err) => {
98 this.disable = false;
99 }
100 );
101 }
102 closeModal() {
103 this.closeDialog.emit("");
104 }
105}
The input boxes will hold a movie’s model. The “Add” button when clicked will call the addNewMovie
method.
The addNewMovie
will collect the movie's name
, imageUrl
, year
, genre
and synopsis
from their input boxes and call the movie resource 'http://localhost:1337/movies'
via the HTTP POST method passing them as payload.
On successful execution from the endpoint, the component refreshes the movies in the movielist
component and closes itself.
editmovie component
Generate editmovie
component by running the below command:
1ng g c editmovie --skip-tests -t -s
See the code:
1import { HttpClient } from "@angular/common/http";
2import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
3@Component({
4 selector: "app-editmovie",
5 template: `
6 <div class="modal">
7 <div class="modal-backdrop" (click)="closeModal()"></div>
8 <div class="modal-content">
9 <div class="modal-header">
10 <h3>Edit Movie</h3>
11 <span style="padding: 10px;cursor: pointer;" (click)="closeModal()"
12 >X</span
13 >
14 </div>
15 <div class="modal-body content">
16 <div class="inputField">
17 <div class="label"><label>Name</label></div>
18 <div>
19 <input id="addMovieName" type="text" value="{{ movie?.name }}" />
20 </div>
21 </div>
22 <div class="inputField">
23 <div class="label"><label>ImageUrl</label></div>
24 <div>
25 <input
26 id="addMovieImageUrl"
27 type="text"
28 value="{{ movie?.imageUrl }}"
29 />
30 </div>
31 </div>
32 <div class="inputField">
33 <div class="label"><label>Synopsis</label></div>
34 <div>
35 <input
36 id="addMovieSynopsis"
37 type="text"
38 value="{{ movie?.synopsis }}"
39 />
40 </div>
41 </div>
42 <div class="inputField">
43 <div class="label"><label>Year</label></div>
44 <div>
45 <input id="addMovieYear" type="text" value="{{ movie?.year }}" />
46 </div>
47 </div>
48 <div class="inputField">
49 <div class="label"><label>Genre</label></div>
50 <div>
51 <input
52 id="addMovieGenre"
53 type="text"
54 value="{{ movie?.genre }}"
55 />
56 </div>
57 </div>
58 </div>
59 <div class="modal-footer">
60 <button (click)="closeModal()">Cancel</button>
61 <button
62 [disabled]="disable"
63 class="btn"
64 (click)="editNewMovie($event)"
65 >
66 Save
67 </button>
68 </div>
69 </div>
70 </div>
71 `,
72 styles: [
73 `
74 .label {
75 padding: 4px 0;
76 font-size: small;
77 color: rgb(51, 55, 64);
78 }
79 .content {
80 display: flex;
81 flex-wrap: wrap;
82 }
83 .inputField {
84 margin: 3px 7px;
85 flex: 1 40%;
86 }
87 `,
88 ],
89})
90export class EditmovieComponent implements OnInit {
91 @Output() closeDialog = new EventEmitter();
92 @Output() refreshMovies = new EventEmitter();
93 @Input() movie: any;
94 disable = false;
95 constructor(private http: HttpClient) {}
96 ngOnInit(): void {}
97 editNewMovie(e: Event) {
98 this.disable = true;
99 const {
100 addMovieName,
101 addMovieYear,
102 addMovieGenre,
103 addMovieImageUrl,
104 addMovieSynopsis,
105 } = window as any;
106 this.http
107 .put("http://localhost:1337/movies/" + this.movie?.id, {
108 name: addMovieName.value,
109 year: addMovieYear.value,
110 synopsis: addMovieSynopsis.value,
111 imageUrl: addMovieImageUrl.value,
112 genre: addMovieGenre.value,
113 })
114 .subscribe(
115 (data) => {
116 this.disable = false;
117 this.closeDialog.emit("");
118 window.location.reload();
119 },
120 (err) => {
121 this.disable = false;
122 }
123 );
124 }
125 closeModal() {
126 this.closeDialog.emit("");
127 }
128}
Does the same thing as addmovie
but this edits a movie.
On initial render, the component gets the movie to edit via its movie
input property. It sets the UI input boxes with their corresponding movie property.
The “Save” button calls the editNewMovie($event)
method. The method collects the values in the input boxes and calls the movie resource 'http://localhost:1337/movies/' + this.movie?.id
passing them as payload. Then, on successful execution on the backend, it closes itself and reloads the page so the edited movie will display the new values.
styles.css
We will add our general styling code in the styles.css
1/* You can add global styles to this file, and also import other style files */
2html {
3 margin: 0;
4 padding: 0;
5}
6body {
7 margin: 0;
8 padding: 0;
9 background-color: rgba(232, 232, 232, 1);
10}
11button {
12 margin-right: 0px;
13 margin-left: 1rem;
14 max-width: 100%;
15 overflow: hidden;
16 text-overflow: ellipsis;
17 white-space: nowrap;
18 text-align: center;
19 outline: 0px;
20 cursor: pointer;
21}
22button:disabled {
23 opacity: 0.5;
24 cursor: not-allowed;
25}
26.btn {
27 height: 30px;
28 padding: 0px 15px 2px;
29 font-weight: 600;
30 font-size: 1rem;
31 line-height: normal;
32 border-radius: 2px;
33 cursor: pointer;
34 outline: 0px;
35 background-color: rgb(0, 126, 255);
36 border: 1px solid rgb(0, 126, 255);
37 color: rgb(255, 255, 255);
38}
39.btn-danger {
40 background-color: red;
41 border: 1px solid red;
42}
43.modal {
44 position: fixed;
45 top: 0;
46 left: 0;
47 width: 100%;
48 height: 100%;
49 display: flex;
50 flex-direction: column;
51 align-items: center;
52 z-index: 1000;
53 font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
54}
55.modal-backdrop {
56 opacity: 0.5;
57 width: inherit;
58 height: inherit;
59 background-color: grey;
60 position: fixed;
61}
62.modal-body {
63 padding: 5px;
64 padding-top: 15px;
65 padding-bottom: 15px;
66}
67.modal-footer {
68 padding: 15px 5px;
69 display: flex;
70 justify-content: space-between;
71}
72.modal-header {
73 display: flex;
74 justify-content: space-between;
75 align-items: center;
76}
77.modal-header h3 {
78 margin: 0;
79}
80.modal-content {
81 background-color: white;
82 z-index: 1;
83 padding: 10px;
84 margin-top: 10px;
85 width: 520px;
86 box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14),
87 0px 9px 46px 8px rgba(0, 0, 0, 0.12);
88 border-radius: 4px;
89}
90input[type="text"] {
91 width: 100%;
92 padding: 9px;
93 font-weight: 400;
94 cursor: text;
95 outline: 0px;
96 border: 1px solid rgb(227, 233, 243);
97 border-radius: 2px;
98 color: rgb(51, 55, 64);
99 background-color: transparent;
100 box-sizing: border-box;
101}
102input:active {
103 border-color: rgb(0, 126, 255);
104}
Now, we have fleshed out our components let’s add our routes.
Add routing
We will add routing to our app. We will have two routes:
/
: The base path. It will render the movielist
component to display the movies in our system.movie/:id
: This path will display a particular movie. It will render the movieview
component.Open the app-routing.module.ts
, and the code to the routes
array.:
1...
2const routes: Routes = [
3 {
4 path: "",
5 component: MovielistComponent,
6 },
7 {
8 path: "movie/:id",
9 component: MovieviewComponent,
10 },
11];
12...
The path ""
will load the MovielistComponent
when we navigate to localhost:4200
in our browser.
The path "movie/:id"
will load the MovieviewComponent
when we navigate to localhost:4200/movie/:id
in our browser. The id
is the id number for the movie.
With this, we have set our routes.
We need to add the HttpClientModule
in our AppModule
this is to make sure we can access the HttpClient
from our components.
Open app.module.ts
and do the below:
1...
2import { HttpClientModule } from '@angular/common/http';
3@NgModule({
4 ...
5 imports: [BrowserModule, AppRoutingModule, HttpClientModule],
6 ...
7})
8export class AppModule {}
Import the HttpClientModule
from '@angular/common/http'
and add it to the imports
array.
We have to run our app.
Run the app
I hope the Strapi backend is still running, if not start it up.
To run the Angular server, type the command below:
1ng serve
This will serve the Angular app at localhost:4200
.
Test the Angular
Now, we will test the Angular to see how it will connect to the Strapi backend.
Already we have two movies in our backend, so loading this: localhost:4200
will show the movies:
Let’s add a new movie. Click on the “Add Movie”.
Type in the content:
1Name -> Captain America: The First Avenger
2ImageUrl -> http://www.movienewsletters.net/photos/277218R1.jpg
3Year -> 2008
4Genre -> Sci-Fi
5Synopsis -> During World War II, Steve Rogers decides to volunteer in an experiment that transforms his weak body. He must now battle a secret Nazi organization headed by Johann Schmidt to defend his nation.
Click on “Add”. Boom!!
Let’s view the full details. Click on the Captain America: The First Avenger
card.
Let’s edit the movie. Click on “Edit”.
Let’s change the imageUrl
to this https://m.media-amazon.com/images/M/MV5BMTYzOTc2NzU3N15BMl5BanBnXkFtZTcwNjY3MDE3NQ@@._V1_UY1200_CR69,0,630,1200_AL_.jpg
a new image poster, and also we change the name to Captain America I
.
Click on “Save”
It changed!! :)
Let’s delete the movie.
Click on “Delete”
Click on “OK”
The Captain America I
is gone.
Source code
Google developed Angular, a front-end web application framework for building single-page applications (SPAs). It offers tools for creating components, handling routing, and managing state, making it a comprehensive solution for front-end development. Angular's framework enables developers to build responsive and interactive user interfaces that enhance the user experience.
Combining Strapi and Angular allows for rapid development of a full-stack movie application. Strapi handles the backend content management, providing Strapi REST and GraphQL APIs for movie data, while Angular manages the frontend, displaying and interacting with that data. This setup also supports omnichannel publishing with Strapi, enabling you to deliver content across multiple platforms seamlessly. By combining these technologies, you can build an interactive user interface that consumes Strapi's APIs to display movie listings, details, and other essential features for a movie app. Organizations like AECOM have successfully transitioned to Strapi to modernize their content management systems.
Enhancing your movie app with additional features can greatly improve user engagement. One such feature is including user reviews and ratings for each movie. Additionally, you might consider implementing real-time data with Strapi to enhance user engagement.
In the Strapi admin panel, create a new collection type named Review with the following fields:
Users can now associate their reviews with specific movies.
Update the public role permissions to allow access to reviews. Navigate to Settings > Roles > Public, and under the Review permissions, enable find and findOne.
1getReviewsByMovie(movieId: number): Observable<any> {
2 return this.http.get(`${this.apiUrl}/reviews?filters[movie][id][$eq]=${movieId}`);
3}
In the movie-detail.component.ts
, fetch the reviews for the selected movie:
1this.movieService.getReviewsByMovie(this.movie.id).subscribe(
2 (response) => {
3 this.reviews = response.data;
4 },
5 (error) => {
6 console.error('Error fetching reviews:', error);
7 }
8);
In the movie-detail.component.html
, add the following code to display the reviews:
1<div *ngIf="reviews && reviews.length">
2 <h3>User Reviews</h3>
3 <div *ngFor="let review of reviews">
4 <p><strong>{{ review.attributes.author }}</strong> rated it {{ review.attributes.rating }}/10</p>
5 <p>{{ review.attributes.content }}</p>
6 </div>
7</div>
This code displays all reviews for the movie, showing the author's name, their rating, and their review content.
When building the Angular frontend, properly handling errors in your components is essential for a reliable application. In the movie-list.component.ts
, implement error handling within the loadMovies
method to catch any issues when fetching data from the Strapi backend:
1loadMovies(): void {
2 this.movieService.getMovies().subscribe(
3 (response) => {
4 this.movies = response.data;
5 },
6 (error) => {
7 console.error('Error fetching movies:', error);
8 }
9 );
10}
By logging errors to the console, you can identify and troubleshoot problems during data retrieval. Ensure that similar error handling is present in other components, such as movie-detail.component.ts
:
1ngOnInit(): void {
2 const id = this.route.snapshot.paramMap.get('id');
3 this.movieService.getMovie(Number(id)).subscribe(
4 (data) => {
5 this.movie = data.data;
6 },
7 (error) => {
8 console.error('Error fetching movie details:', error);
9 }
10 );
11}
Testing your components in the browser's developer tools allows you to monitor console outputs and network requests. Use these tools to inspect errors and verify that the application behaves as expected. Regularly testing during development helps in catching bugs early and ensures a smoother user experience.
Conclusion
We learned a lot in this tutorial. First, we introduced Strapi and learned what it is and the powerful punch it packs. Next, we demonstrated this by building a movie backend and consuming the API endpoints from an Angular app.
See that it took us less time to build our API without the need for starting the backend from scratch or setting up backend tools. It was seamless. We need to call the API endpoints from our frontend app to get the job done.
If you have any questions regarding this or anything I should add, correct, or remove, feel free to submit your comments.
Author of "Understanding JavaScript", Chidume is also an awesome writer about JavaScript, Angular, React and other web technologies.