43
loading...
This website collects cookies to deliver better user experience
Animals
. Strapi will provide us with the endpoints:/animals
GET/animals/:id
GET/animals/:id
PUT/animals/:id
DELETE/animals
POST/animals
GET: This endpoint will return all the animals on the server./animals/:id
GET: This will return a specific animal from the server using the id to find the animal. The id is a globally unique identifier set by the server to identify/marl each animal resource in the backend uniquely./animals/:id
PUT: This edits an animal resource in the collection. The id is the id of the animal to edit. This request body will contain the new info of the animal that will be edited./animals/:id
DELETE: This endpoint deletes/removes an animal from the collection./animals
POST: This endpoint adds a new animal to the mix. The request body of this will contain the data of the new animal to be created.offset-based pagination
.1. data_1
2. data_2
3. data_3
4. data_4
5. data_5
6. data_6
7. data_7
8. data_8
9. data_9
10. data_10
11. data_11
12. data_12
13. data_13
14. data_14
limit: 5
offset: 0
1. data_1
and collect 5 records below it. The result will be:1. data_1
2. data_2
3. data_3
4. data_4
5. data_5
limit: 5
offset: 5
6. data_6
7. data_7
8. data_8
9. data_9
10. data_10
SELECT column FROM table LIMIT 10 OFFSET 10
LIMIT
states the number of rows to retrieve/return from the table. The OFFSET
tells the SQL engine to start from the 11th row in the table. With the above SQL statement, we have achieved offset-based pagination in SQL.data_1
is removed, the indexes change, and it affects the next set of records to be fetched because offset pagination works on the indexes. This results in missing records or duplicates records.cursor: 2
limit: 5
id
as the cursor in the records field. This request will start from the record with an id
field with 2 and collect 5 records below it.select * from blogPosts where id > 0 limit 2
blogPosts
table starting from the record whose id
field is greater than 0. Thus, the maximum number of blog post rows to select is 2 records only.blogPosts
table is this:{ id: 1, post: "Post_1"},
{ id: 2, post: "Post_2"},
{ id: 3, post: "Post_3"},
{ id: 4, post: "Post_4"},
{ id: 5, post: "Post_5"},
{ id: 6, post: "Post_6"},
{ id: 7, post: "Post_7"},
{ id: 8, post: "Post_8"},
{ id: 9, post: "Post_9"},
{ id: 10, post: "Post_10"}
{ id: 1, post: "Post_1"},
{ id: 2, post: "Post_2"},
id
field value is greater than 2. This is because the last record in our result has an id
of 2.select * from blogPosts where id > 2 limit 2
query {
posts {
title
body
}
}
title
and body
fields present.{
"data": [
{
"title": "Intro to React",
"body": "Body content of React"
},
{
"title": "Intro to Angular",
"body": "Body content of Angular"
},
{
"title": "Intro to Vue",
"body": "Body content of Vue"
},
{
"title": "Intro to Svelte",
"body": "Body content of Svelte"
},
{
"title": "Intro to Preact",
"body": "Body content of Preact"
},
{
"title": "Intro to Alpine",
"body": "Body content of Alpine"
}
]
}
limit
and offset
arguments are used to implement offset-based pagination in GraphQL endpoints.limit
sets the number of records to return from the endpoint. The offset
sets the index in the dataset to start from.query {
posts(limit: 2, offset: 7) {
title
body
}
}
query {
posts(limit: 2, offset: 9) {
title
body
}
}
query {
posts(limit: 2, offset: 11) {
title
body
}
}
limit
and offset
args and use them to return the records.Query: {
posts: (parent, args, context, info) => {};
}
args
param will have the arguments in our query in its object body. So we destructure them:Query: {
posts: (parent, args, context, info) => {
const { limit, offset } = args
...
};
}
const postArray = [];
Query: {
posts: (parent, args, context, info) => {
const { limit, offset } = args;
return postsArray.slice(offset, limit);
};
}
Array#slice
method to get the posts off the postsArray
using the limit
and offset
as the starting index and the amount to slice, respectively.limit
and offset
arguments. We can then use them to get records in parts from the database we are using (e.g., MongoDB, MySQL, in-memory database, etc.)cursor
and limit
arguments. The argument's names can be whatever you want in your implementation, and we chose these names to describe what they do.query {
posts(cursor: 4, limit: 7) [
title
body
]
}
cursor
is set to 4, this is the id of the record in the dataset to start from, and the limit
is the number of records to return.id
of the records in the list. The cursor can be any field in your records; the important thing is that the cursor should be globally unique in your records. Strapi supports GraphQL, and this is done by installing the GraphQL plugin to the Strapi mix.start
and limit
filters to achieve offset-based pagination in our Strapi endpoint. Now, we build a GraphQL Strapi API to demonstrate how to use pagination in GraphQL-Strapi.newsapp-gpl
:➜ mkdir newsapp-gpl
➜ strapi-graphql-pagination cd newsapp-gpl
➜ newsapp-gpl yarn create strapi-app newsapp-gpl-api --quickstart
newsapp-GPL-API
and also start the Strapi server at localhost:1337
. This is the URL from where we can build our collections and also call the collections endpoints.➜ newsapp-gpl-api yarn strapi install graphql
Ctrl+C
in the terminal and then, run:yarn develop
http://localhost:1337/graphql
. the GraphQL playground will open up.mutation {
register(input: { username: "nnamdi", email: "[email protected]", password: "nnamdi" }) {
jwt
user {
username
email
}
}
}
User
collection type in our admin panel."jwt"
returned when we registered, we will pass it in the "Authorization" header on every request like this:{ "Authorization": "Bearer YOUR_JWT_GOES_HERE" }
newsPost
collection and add the fields:title -> Text
body -> Text
imageUrl -> Text
writtenBy -> Text
newsPost
collection. Instead, it will create GraphQL mutations and queries for the newsPost
collection.// NewsPost's Type definition
type NewsPost {
id: ID!
created_at: DateTime!
updated_at: DateTime!
title: String
body: String
imageUrl: String
writtenBy: String
published_at: DateTime
}
type Query {
// gets a single new post via its id
newsPost(id: ID!, publicationState: PublicationState): NewsPost
// returns all news posts
newsPosts(
sort: String
limit: Int
start: Int
where: JSON
publicationState: PublicationState
): [NewsPost]
// This gives us more leverage on what to return in the query. E.g, it provides an aggregator that we can use to get the total count of news post data in the backend.
newsPostsConnection(
sort: String
limit: Int
start: Int
where: JSON
): NewsPostConnection
}
type Mutation {
// creates a new news post
createNewsPost(input: createNewsPostInput): createNewsPostPayload
// updates/edits a news post
updateNewsPost(input: updateNewsPostInput): updateNewsPostPayload
// delete a news post
deleteNewsPost(input: deleteNewsPostInput): deleteNewsPostPayload
}
newsPost
collection. Go to "Settings" -> "USERS & PERMISSIONS PLUGIN " section "Roles". Enable "Select all" for newsPost
. Then, scroll up and click on "Save".create-react-app
CLI tool is already installed in your system. IF not run the below command to install it:npm i create-react-app -g
newsapp-gpl
folder.create-react-app newsapp-strapi
create-react-app
create a React.js project in a newsapp-strapi
folder.cd newsapp-strapi
npm i react-router-dom axios
react-router-dom
will be used to add routing to our app.axios
an HTTP library, we will use this to perform HTTP requests to our Strapi GraphQL endpoints.npm run start
localhost:3000
./news
: This route will render all the news in our app./newspost/:id
: This route will render a particular news post. The id will be the id of the news post./news
route. It will display the list of news. It is an intelligent component.NewsList
component will render it./newspost/:id
is navigated to.pages
and components
folders.mkdir src/pages src/components
Header
, NewsCard
, AddNewsDialog
components will be in the components
folder.NewsList
, NewsView
will be in the pages folder.mkdir src/components/AddNewsDialog
touch src/components/AddNewsDialog/index.js
mkdir src/components/Header
touch src/components/Header/index.js
touch src/components/Header/Header.css
mkdir src/components/NewsCard
touch src/components/NewsCard/index.js
touch src/components/NewsCard/NewsCard.css
mkdir src/pages/NewsList
touch src/pages/NewsList/index.js
touch src/pages/NewsList/NewsList.css
mkdir src/pages/NewsView
touch src/pages/NewsView/index.js
touch src/pages/NewsView/NewsView.css
App.js
and paste the below code:import "./App.css";
import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom";
import Header from "./components/Header";
import NewsList from "./pages/NewsList";
import NewsView from "./pages/NewsView";
function App() {
return (
<>
<Header />
<div className="container">
<head>
<title>NewsNet</title>
<link rel="icon" href="/favicon.ico" />
</head>
<main className="main">
<BrowserRouter>
<Switch>
<Route path="/news">
<NewsList />
</Route>
<Route path="/newspost/:id">
<NewsView />
</Route>
<Route exact path="/">
<Redirect to="/news" />
</Route>
<Route path="*">
<NewsList />
</Route>{" "}
</Switch>
</BrowserRouter>
</main>
</div>
</>
);
}
export default App;
news
route will render the NewsList
component and the route newspost/:id
will render the NewsView
component.BrowserRouter
tag whenever the route changes. Every other thing outside the BrowserRouter
tag will render on every route.Header
component and rendered it outside the BrowserRouter
so it appears on all pages. Then, we set the head title using the title
.Header
component:import "./Header.css";
export default function Header() {
return (
<section className="header">
<div className="headerName">NewsNet</div>
</section>
);
}
.header {
height: 54px;
background-color: rgba(234, 68, 53, 1);
color: white;
display: flex;
align-items: center;
padding: 10px;
font-family: sans-serif;
/*width: 100%;*/
padding-left: 27%;
}
.headerName {
font-size: 1.8em;
}
NewsList
component:import "./NewsList.css";
import NewsCard from "./../../components/NewsCard";
import { useEffect, useState } from "react";
import axios from "axios";
import AddNewsDialog from "../../components/AddNewsDialog";
export default function NewsList() {
const [newsList, setNewsList] = useState([]);
const [showModal, setShowModal] = useState(false);
const [start, setStart] = useState(0);
const [limit] = useState(2);
const [pageDetails, setPageDetails] = useState();
useEffect(() => {
async function fetchNews() {
const data = await axios.post("http://localhost:1337/graphql", {
query: `query {
newsPostsConnection(limit: ${limit}, start: ${start}) {
values {
id
title
body
writtenBy
imageUrl
created_at
}
aggregate {
totalCount
}
}
}`,
});
setPageDetails(data?.data?.data?.newsPostsConnection?.aggregate);
setNewsList([...data?.data?.data?.newsPostsConnection?.values]);
//window.location.reload();
}
fetchNews();
}, [start]);
function nextPage() {
setStart(limit + start);
}
function prevPage() {
setStart(start - limit);
}
function showAddNewsDialog() {
setShowModal(!showModal);
}
return (
<div className="newslist">
<div className="newslistbreadcrumb">
<div className="newslisttitle">
<h3>World News</h3>
</div>
<div style={{ display: "flex", alignItems: "center" }}>
<div style={{ marginRight: "4px" }}>
<button onClick={showAddNewsDialog}>Add News</button>
</div>
</div>
</div>
<div>
{newsList
?.sort((a, b) => b.created_at.localeCompare(a.created_at))
?.map((newsItem, i) => (
<NewsCard newsItem={newsItem} key={i} />
))}
</div>
{showModal ? <AddNewsDialog closeModal={showAddNewsDialog} /> : null}
<div>
<span>
<button disabled={limit > start} onClick={prevPage}>
Prev
</button>
</span>
<span>
<button
disabled={pageDetails && start + limit >= pageDetails?.totalCount}
onClick={nextPage}
>
Next
</button>
</span>
</div>
</div>
);
}
start
, limit
, newsList
, showModal
, pageDetails
state. The start state holds the current offset. The limit state has the limit of news post records to return. newsList
state holds the current list of news posts already fetched. The pageDetails
has the total count of news posts in the backend.newsPostConnection
query. We used newsPostConnection
to use the aggregate
field to get the total count of news posts in our backend. See that we passed in limit
and start
as arguments in the query with the states' values start
and limit
.newsPostConnection
query we set the fields we need in the news post field.Axios
to send the query to the Strapi GraphQL backend. We use HTTP POST because GraphQL comms via the POST method, we set the query as the payload, we do this by setting the query string as data in the POST body, the query
prop in the body is what we use to set the query string. request.body.query
, and the GraphQL runtime will execute the query.ApolloClient
library for the GraphQL query requests, but I decided to use Axios to learn how GraphQL queries can be sent without using the ApolloClient
lib. It is still the same old way of sending HTTP requests; it's just that ApolloClient
abstracts that away and provides many features to make GraphQL queries efficient and straightforward.totalCount
from the data return from the HTTP request and store it in the pageDetails
state. Also, we retrieve the news list in the data and save it in the newsList
state.useEffect
hook callback. This hook will run whenever the component mounts or re-renders. We set the start
state as a dependency in the useEffect
, and this will cause the useEffect
to run only when the start
value changes.nextPage
and prevPage
functions. The nextPage
function sets the next offset to start
from. The math here is that the next offset will be from adding the limit
to the current start. limit
from the start. All these are set in the start
state and will cause the component to render, and the query newsPostConnection
will be called with the new start
value. This gives us a new news post.newsList
state. We see the Prev
and Next
buttons. These buttons are what we use to navigate the pages. Next
loads the next news posts, and the Prev
loads the previous page. Also, the Next
button is disabled when there is no next data, and the Prev
is disabled when there is no previous page.NewsView
component. This component will load a piece of particular news and display its details.import "./NewsView.css";
import { useParams } from "react-router-dom";
import axios from "axios";
import { useEffect, useState } from "react";
export default function NewsView() {
let { id } = useParams();
const [news, setNews] = useState();
useEffect(() => {
async function getNews() {
const data = await axios.post("http://localhost:1337/graphql", {
query: `
query {
newsPost(id: ${id}) {
id
title
body
imageUrl
writtenBy
created_at
}
}`,
});
setNews(data?.data?.data?.newsPost);
}
getNews();
}, []);
async function deleteNews() {
if (window.confirm("Do you want to delete this news?")) {
await axios.post("http://localhost:1337/graphql", {
query: `
mutation {
deleteNewsPost(input: {where: {id: ${id} }}) {
newsPost {
title
}
}
}`,
});
window.history.pushState(null, "", "/news");
window.location.reload();
}
}
return (
<div className="newsview">
<div style={{ display: "flex" }}>
<a className="backHome" href="/news">
Back
</a>
</div>
<div
className="newsviewimg"
style={{ backgroundImage: `url(${news?.imageUrl})` }}
></div>
<div>
<div className="newsviewtitlesection">
<div className="newsviewtitle">
<h1>{news?.title}</h1>
</div>
<div className="newsviewdetails">
<span style={{ flex: "1", color: "rgb(99 98 98)" }}>
Written By: <span>{news?.writtenBy}</span>
</span>
<span style={{ flex: "1", color: "rgb(99 98 98)" }}>
Date: <span>{news?.created_at}</span>
</span>
<span>
<button className="btn-danger" onClick={deleteNews}>
Delete
</button>
</span>
</div>
</div>
<div className="newsviewbody">{news?.body}</div>
</div>
</div>
);
}
useParams
hook to get the id
off the URL newspost/:id
. This id
value is used to get the news details.http://localhost:1337/graphql
endpoint and passed the query newsPost
in the body in the query
prop. The id
is passed to the id
argument in the query. This query will fetch the news post and set it to the newsPost
state.Delete
button calls the deleteNews
function. This function sends a deleteNewsPost
mutation to our Strapi GraphQL endpoint. The id
is passed to the id
argument in the mutation. After that, we navigate to the main page.Back
navigates us back to the news page:.newsview {
margin-top: 7px;
}
.backHome {
/*height: 30px; */
padding: 6px 26px;
font-weight: 400;
font-size: 1rem;
line-height: normal;
border-radius: 2px;
cursor: pointer;
outline: 0px;
background-color: rgba(234, 68, 53, 1); /* rgb(0, 126, 255);*/
border: 1px solid rgb(234, 68, 53); /*rgb(0, 126, 255);*/
color: rgb(255, 255, 255) !important;
text-align: center;
margin: 3px;
}
.newsviewimg {
background-color: darkgray;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
height: 200px;
}
.newsviewdetails {
display: flex;
justify-content: space-between;
align-items: center;
}
.newsviewtitlesection {
margin-bottom: 20px;
}
.newsviewtitle h1 {
margin-bottom: 6px;
}
.newsviewbody {
font-size: large;
}
.newsviewbody::first-letter {
font-weight: 700;
font-size: 4em;
line-height: 0.83;
float: left;
margin-right: 7px;
margin-bottom: 4px;
color: rgba(234, 68, 53, 1);
}
.newsviewbody {
clear: left;
font-size: 21px;
line-height: 1.58;
letter-spacing: -0.003em;
}
NewsCard
and AddNewsDialog
.NewsList
to display little details about each news on the main page.import { Link } from "react-router-dom";
import "./NewsCard.css";
export default function NewsCard({ newsItem }) {
const { title, body, imageUrl, id } = newsItem;
const synopsis = body.slice(0, 150);
return (
<Link to={"/newspost/" + id}>
<div className="newscard">
<div
className="newscardimg"
style={{ backgroundImage: `url(${imageUrl})` }}
></div>
<div style={{ flex: "1 1 203%" }}>
<div className="newscardtitle">
<h1>{title.slice(0, 30)}</h1>
</div>
<div>
<span>{synopsis}</span>
</div>
<div></div>
</div>
</div>
</Link>
);
}
newsItem
argument, and the details are destructured and rendered..newscard {
/*background-color: white;*/
padding: 8px;
/*box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
transition: 0.3s;*/
border-radius: 4px;
margin: 8px;
cursor: pointer;
display: flex;
}
.newscardimg {
width: 146px;
height: 146px;
background-color: darkgray;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
margin-right: 9px;
flex: 1 100%;
}
.newscardtitle {
flex: 1 100%;
}
.newscardtitle h1 {
margin-top: 0;
margin-bottom: 1px;
}
AddNewsDialog
import { useState } from "react";
import axios from "axios";
export default function AddNewsDialog({ closeModal }) {
const [disable, setDisable] = useState(false);
async function saveNews() {
const title = window.newsTitle.value;
const imageUrl = window.newsImageUrl.value;
const writtenBy = window.newsWrittenBy.value;
const body = window.newsBody.value;
setDisable(true);
await axios.post("http://localhost:1337/graphql", {
query: `
mutation {
createNewsPost(input: { data: { title: "${title}", body: "${body}", imageUrl: "${imageUrl}", writtenBy: "${writtenBy}"}}) {
newsPost {
id
title
body
writtenBy
created_at
}
}
}
`,
});
window.location.reload();
setDisable(false);
}
return (
<div className="modal">
<div className="modal-backdrop" onClick={closeModal}></div>
<div className="modal-content">
<div className="modal-header">
<h3>Add News</h3>
<span
style={{ padding: "10px", cursor: "pointer" }}
onClick={closeModal}
>
X
</span>
</div>
<div className="modal-body content">
<div style={{ display: "flex", flexWrap: "wrap" }}>
<div className="inputField">
<div className="label">
<label>Title</label>
</div>
<div>
<input id="newsTitle" type="text" />
</div>
</div>
<div className="inputField">
<div className="label">
<label>ImageUrl</label>
</div>
<div>
<input id="newsImageUrl" type="text" />
</div>
</div>
<div className="inputField">
<div className="label">
<label>Written By</label>
</div>
<div>
<input id="newsWrittenBy" type="text" />
</div>
</div>
<div className="inputField" style={{ flex: "2 1 100%" }}>
<div className="label">
<label>Body</label>
</div>
<div>
<textarea
id="newsBody"
style={{ width: "100%", height: "200px" }}
></textarea>
</div>
</div>
</div>
</div>
<div className="modal-footer">
<button
disabled={disable}
className="btn-danger"
onClick={closeModal}
>
Cancel
</button>
<button disabled={disable} className="btn" onClick={saveNews}>
Save
</button>
</div>
</div>
</div>
);
}
saveNews
function is called by the Save
button when clicked. The function collects the news details from the input boxes and sends a mutation to our Strapi GraphQL endpoint http://localhost:1337/graphql. query
object prop, and the mutation is createNewsPost
its input argument has the news details picked from the UI: body
, title
, writtenBy
, and imageUrl
. The page is reloaded, so the new addition is displayed.index.css
file:body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: rgba(234, 238, 243, 1);
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
button {
height: 30px;
padding: 0px 15px 2px;
font-weight: 400;
font-size: 1rem;
line-height: normal;
border-radius: 2px;
cursor: pointer;
outline: 0px;
background-color: rgba(234, 68, 53, 1); /* rgb(0, 126, 255);*/
border: 1px solid rgb(234, 68, 53); /*rgb(0, 126, 255);*/
color: rgb(255, 255, 255);
text-align: center;
margin: 3px;
}
.btn-danger {
background-color: rgb(195 18 18);
border: 1px solid rgb(195 18 18);
}
.container {
min-height: 100vh;
/*padding: 0 0.5rem; */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background-color: rgba(234, 238, 243, 1);
}
.main {
/*padding: 5rem 0;*/
flex: 1;
display: flex;
flex-direction: column;
width: 46%;
/*justify-content: center;
align-items: center;*/
}
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1000;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
.modal-backdrop {
opacity: 0.5;
width: inherit;
height: inherit;
background-color: grey;
position: fixed;
}
.modal-body {
padding: 5px;
padding-top: 15px;
padding-bottom: 15px;
}
.modal-footer {
padding: 15px 5px;
display: flex;
justify-content: space-between;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
}
.modal-content {
background-color: white;
z-index: 1;
padding: 10px;
margin-top: 10px;
width: 520px;
box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14),
0px 9px 46px 8px rgba(0, 0, 0, 0.12);
border-radius: 4px;
}
input[type="text"] {
width: 100%;
/*height: 3.4rem;*/
padding: 9px;
font-weight: 400;
/*font-size: 1.3rem;*/
cursor: text;
outline: 0px;
border: 1px solid rgb(227, 233, 243);
border-radius: 2px;
color: rgb(51, 55, 64);
background-color: transparent;
box-sizing: border-box;
}
.label {
padding: 4px 0;
font-size: small;
color: rgb(51, 55, 64);
}
.content {
display: flex;
flex-wrap: wrap;
flex-direction: column;
}
.inputField {
margin: 3px 7px;
flex: 1 40%;
}
button:disabled,
button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
a[href] {
text-decoration: none;
color: black;
}
a:visited {
color: black;
}
localhost:3000
. Then, press the Next
and Prev
buttons to navigate the pages.