Intro
This article is dedicated for a web developer who appreciates design freedom, yet who'd like to code less in a setup-free web-based development environment.
It's a "how to" integrate with Strapi using GlueCodes Studio - the tool powering your every-day work in the ways you haven't seen elsewhere. It's for somebody who'd be pleased with loads of automation to deliver an extremely fast and scalable code i.e. build-time diffed JSX using SolidJS and organised around an implicit uni-directional data flow. Obviously you can use it for FREE. Without further "context drawing", let's begin.
What are we building?
We're going to use Strapi as a headless CMS. It comes with a hosted demo for an imaginary Food Advisor site and it's already seeded with restaurant data. You can request your instance here. After filling in a form, you'll receive an email with few URLs. Mine looked like these:
Demo URL: https://api-hi2zm.strapidemo.com/admin
API restaurants URL: https://api-hi2zm.strapidemo.com/restaurants
GraphQL URL: https://api-hi2zm.strapidemo.com/graphql
Credentials: john@doe.com / welcomeToStrapi123
Don't try to be a smartass, the URLs won't work longer that the demo duration you provided in the demo form.
I won't be covering how to use Strapi, just explore it yourself if you like. For our tutorial all you'll need is these two URLs:
GraphQL:
https://api-{someHash}.strapidemo.com/graphql
Image Server:
https://api-{someHash}.strapidemo.com
Our app will have the following features:
- grid of restaurants with names, description, category and image
- filtering by category
- filtering by neighborhood
- filtering by language
- pagination
The app will apply the filters without the browser hard-reload, meaning it'll be SPA. In Part 1, we will focus on the Strapi integration and leave pagination and mobile responsiveness for Part 2. I'll leave any styling improvements to you as it isn't a CSS tutorial. It'll look like this:
Coding
First, you'll need go to: GlueCodes Studio. You'll be asked to sign up via Google or Github. No worries, it won't require any of your details. Once you're in the project manager, choose "Strapi Food Advisor" template. You'll be asked to choose a directory where the project suppose to be stored. Just choose one and you should be redirected to IDE.
You might be welcomed with some introjs walk-through(s) guiding you around something like this:
As mentioned above, you'll need two URLs:
GraphQL:
https://api-{someHash}.strapidemo.com/graphql
Image Server:
https://api-{someHash}.strapidemo.com
Let's add them to Global Variables as GQL_URL
and IMAGE_BASE_URL
:
Now you can click "Preview" to see the working app.
App data flow design
We'll need a list of restaurants pulled from Strapi's GraphQL API. GlueCodes Studio has a built-in data flow management. Your business logic is spread across app actions which store their returned/resolved values in a single object store. The data changes flow in one direction and UI reacts to changes of the store, updating the only affected parts. The DOM diffing happens in-compilation time and is powered by SolidJS.
There are two types of actions; the ones that supply data before rendering called providers and those triggered by a user called commands. Their both returned/resolved values are accessible from a single object store by their own names. In your UI, you get access to global variables: actions
and actionResults
. The variable actions
is an object of Commands you can call to perform an action e.g. to return/resolve fetched data. You can read more in docs. It's really easier done than said so bear with me.
The API call we're going use returns restaurants along with categories. Our app also needs a list of neighborhoods and parse URL query parameters to affect the GraphQL call. We'll also need some basic data transformations before passing it to our UI. Based on this information, I decided to have the following providers:
- fetchRestaurantData
- getCategories
- getLanguages
- getNeighborhoods
- getRestaurants
- parseUrlQueryParams
For filtering, we'll need the following commands:
- changeCategory
- changeLanguage
- changeNeighborhood
I'll walk you through them one by one but before, you need to understand the mechanism of providers a bit further. Note that providers, when returning they implicitly write to a single object store by their own names. Then, a snapshot of this store is passed from one provider to another. It means you can access results of the previously called providers. It also means you need to set their execution order. It's done by navigating to a particular provider and clicking "Run After" button and in its corresponding pane, choose which providers need to be executed before. You can expect something like this:
We want to achieve the following pipeline:
The fetchRestaurantData
uses a result of parseUrlQueryParams
.
The getRestaurants
and getCategories
use a result of fetchRestaurantData.
It can look like this:
- getNeighborhoods
- parseUrlQueryParams
- fetchRestaurantData
- getRestaurants
- getLanguages
- getCategories
OK, let's dive into functions now.
Actions
providers/fetchRestaurantData
:
export default async (actionResults) => {
const { category, district, locale } = actionResults.parseUrlQueryParams
const where = {
locale: 'en'
}
if (category !== 'all') {
where.category = category
}
if (district !== 'all') {
where.district = district
}
if (locale) {
where.locale = locale
}
const query = `
query ($limit: Int, $start: Int, $sort: String, $locale: String, $where: JSON) {
restaurants(limit: $limit, start: $start, sort: $sort, locale: $locale, where: $where) {
id
description
district
cover {
url
}
category {
name
}
name
locale
localizations {
id
locale
}
note
price
reviews {
note
content
}
}
restaurantsConnection(where: $where) {
aggregate {
count
}
}
categories {
id
name
}
}
`
const records = await (await fetch(global.GQL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query,
variables: {
limit: 15,
start: actionResults.parseUrlQueryParams.start || 0,
sort: 'name:ASC',
locale: 'en',
where
}
})
})).json()
return records.data
}
Notes:
-
actionResults.parseUrlQueryParams
accesses the query URL params -
global.GQL_URL
accesses theGQL_URL
global variable
providers/getCategories
:
export default (actionResults) => {
return [
{
id: 'all',
name: 'All'
},
...actionResults.fetchRestaurantData.categories
]
}
Notes:
-
actionResults.fetchRestaurantData.categories
accesses the categories which are part offetchRestaurantData
result
providers/getLanguages
:
export default () => {
return [
{
id: 'en',
name: 'En'
},
{
id: 'fr',
name: 'Fr'
}
]
}
providers/getNeighborhoods
:
export default () => {
return [
{ name: 'All', id: 'all' },
{ name: '1st', id: '_1st' },
{ name: '2nd', id: '_2nd' },
{ name: '3rd', id: '_3rd' },
{ name: '4th', id: '_4th' },
{ name: '5th', id: '_5th' },
{ name: '6th', id: '_6th' },
{ name: '7th', id: '_7th' },
{ name: '8th', id: '_8th' },
{ name: '9th', id: '_9th' },
{ name: '10th', id: '_10th' },
{ name: '11th', id: '_11th' },
{ name: '12th', id: '_12th' },
{ name: '13th', id: '_13th' },
{ name: '14th', id: '_14th' },
{ name: '15th', id: '_15th' },
{ name: '16th', id: '_16th' },
{ name: '17th', id: '_17th' },
{ name: '18th', id: '_18th' },
{ name: '19th', id: '_19th' },
{ name: '20th', id: '_20th' }
]
}
providers/getRestaurants
:
export default (actionResults) => {
return actionResults.fetchRestaurantData.restaurants
.map((record) => ({
id: record.id,
name: record.name,
description: record.description,
category: record.category.name,
district: record.district,
thumbnail: record.cover[0].url
}))
}
Notes:
-
actionResults.fetchRestaurantData.restaurants
accesses the restaurants which are part offetchRestaurantData
result
providers/parseUrlQueryParams
:
export default (actionResults) => {
return imports.parseUrlQueryParams()
}
Notes:
-
imports.parseUrlQueryParams
accesses an external dependency function.
In GlueCodes Studio you can use any UMD-bundled modules including those in UNPKG. Just click on Dependencies icon and edit the JSON file to look like:
{
"css": {
"bootstrap": "https://unpkg.com/bootstrap@4.5.2/dist/css/bootstrap.min.css",
"fa": "https://unpkg.com/@fortawesome/fontawesome-free@5.14.0/css/all.min.css"
},
"js": {
"modules": {
"parseUrlQueryParams": "https://ide.glue.codes/repos/df67f7a82cbdc5efffcb31c519a48bf6/basic/reusable-parseUrlQueryParams-1.0.4/index.js",
"setUrlQueryParam": "https://ide.glue.codes/repos/df67f7a82cbdc5efffcb31c519a48bf6/basic/reusable-setUrlQueryParam-1.0.4/index.js"
},
"imports": {
"parseUrlQueryParams": {
"source": "parseUrlQueryParams",
"importedName": "default"
},
"setUrlQueryParam": {
"source": "setUrlQueryParam",
"importedName": "default"
}
}
}
}
commands/changeCategory
:
export default (categoryId) => {
imports.setUrlQueryParam({ name: 'category', value: categoryId })
}
Notes:
-
imports.setUrlQueryParam
accesses an external dependency function
commands/changeLanguage
:
export default (languageId) => {
imports.setUrlQueryParam({ name: 'locale', value: languageId })
}
commands/changeNeighborhood
:
export default (neighborhoodId) => {
imports.setUrlQueryParam({ name: 'district', value: neighborhoodId })
}
Structure
In GlueCodes Studio each page is split into logical UI pieces to help you keep your UI modular. A single slot has its scoped CSS which means it can be styled by classes which only affect a given slot and their names can be duplicated in other slots. In the exported code, slots will be extracted to dedicated files making them more maintainable.
To make your HTML dynamic, you can use attribute directives as you would in modern web frameworks. When typing most of them, you'll get notified to auto-create (if don't exist) required commands, providers or to install a widget. The vocabulary is quite simple, attribute [gc-as]
tells what it is and other [gc-*]
attributes are parameters. Note: For any naming attributes use camelcase e.g. for a slot you would use [gc-name="myAwesomeSlot"]
.
Here is a slightly stripped-out index page HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta gc-as="navName" gc-name="Home">
<title>FoodAdvisor</title>
<body>
<div gc-as="layout">
<div class="container-fluid">
<div gc-as="slot" gc-name="header"></div>
<div class="d-flex">
<div gc-as="slot" gc-name="filters"></div>
<div gc-as="slot" gc-name="content">
<div class="contentWrapper">
<h1 class="heading">Best restaurants in Paris</h1>
<div class="grid">
<div gc-as="listItemPresenter" gc-provider="getRestaurants" class="card">
<img-x class="card-img-top thumbnail" alt="Card image cap">
<script>
props.src = `${global.IMAGE_BASE_URL}${getRestaurantsItem.thumbnail}`
</script>
</img-x>
<div class="card-body">
<h4 gc-as="listFieldPresenter" gc-provider="getRestaurants" gc-field="name" class="name">restaurant name</h4>
<h5 gc-as="listFieldPresenter" gc-provider="getRestaurants" gc-field="category" class="category">restaurant category</h5>
<p gc-as="listFieldPresenter" gc-provider="getRestaurants" gc-field="description" class="card-text">restuarant description</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div gc-as="slot" gc-name="footer"></div>
</div>
</div>
</body>
</html>
Notes:
-
<div gc-as="layout">
is the app wrapper. -
<div gc-as="slot" gc-name="content">
is a logical UI piece which has its scoped CSS and is extracted to dedicated file. It requires a unique (within page) camelcase gc-name. Whatever is in slot gets access to a store, commands and other useful variables. You can learn more here. -
<div gc-as="slot" gc-name="filters"></div>
is a reusable slot. Similar to a slot however it can be used across multiple pages. Reusable slots can be understood as partials. You'll be editing reusable slots in a dedicated HTML editor and injecting them in pages using empty slot directive. -
<div gc-as="listItemPresenter" gc-provider="getRestaurants" class="card">
repeats this div over an array returned bygetRestaurants
provider. -
<h4 gc-as="listFieldPresenter" gc-provider="getRestaurants" gc-field="name" class="name">restaurant name</h4>
displays a propertyname
of an item while looping overgetRestaurants
provider.
Let's take a look at this once more:
<img-x class="card-img-top thumbnail" alt="Card image cap">
<script>
props.src = `${global.IMAGE_BASE_URL}${getRestaurantsItem.thumbnail}`
</script>
</img-x>
Static HTML has no built-in way to make it reactive. Hence GlueCodes Studio has a concept called extended tags which is named like: tagName + '-x'
and has an embedded <script>
included. Its code is sandboxed allowing you to access variables which are available inside other directive like slots or list item presenters. The scripts can assign to props
variable to change props/attributes of the extended tag.
Note that when an extended tag is placed inside a list item presenter you get access to a variable called like:
providerName + Item
, in our casegetRestaurantsItem
which is an item while looping overgetRestaurants
provider. You could also accessgetRestaurantsIndex
for a numeric index in the array.
Other Templates:
reusableSlots/filters
:
<div class="wrapper">
<h2 class="heading">Categories</h2>
<ul class="filterSet">
<li gc-as="listItemPresenter" gc-provider="getCategories" class="filterItem">
<label>
<input-x type="radio">
<script>
props.name = 'category'
props.value = getCategoriesItem.id
props.checked = getCategoriesItem.id === (actionResults.parseUrlQueryParams.category || 'all')
props.onChange = (e) => {
actions.changeCategory(e.target.value)
actions.reload()
}
</script>
</input-x>
<span gc-as="listFieldPresenter" gc-provider="getCategories" gc-field="name" class="label">category name</span>
</label>
</li>
</ul>
<h2 class="heading">Neighborhood</h2>
<ul class="filterSet">
<li gc-as="listItemPresenter" gc-provider="getNeighborhoods" class="filterItem">
<label>
<input-x type="radio">
<script>
props.name = 'neighborhood'
props.value = getNeighborhoodsItem.id
props.checked = getNeighborhoodsItem.id === (actionResults.parseUrlQueryParams.district || 'all')
props.onChange = (e) => {
actions.changeNeighborhood(e.target.value)
actions.reload()
}
</script>
</input-x>
<span gc-as="listFieldPresenter" gc-provider="getNeighborhoods" gc-field="name" class="label">neighborhood name</span>
</label>
</li>
</ul>
<h2 class="heading">Language</h2>
<ul class="filterSet">
<li gc-as="listItemPresenter" gc-provider="getLanguages" class="filterItem">
<label>
<input-x type="radio">
<script>
props.name = 'languages'
props.value = getLanguagesItem.id
props.checked = getLanguagesItem.id === (actionResults.parseUrlQueryParams.locale || 'en')
props.onChange = (e) => {
actions.changeLanguage(e.target.value)
actions.reload()
}
</script>
</input-x>
<span gc-as="listFieldPresenter" gc-provider="getLanguages" gc-field="name" class="label">language name</span>
</label>
</li>
</ul>
</div>
reusableSlots/footer
:
<footer class="wrapper">
<p>Try <a href="https://www.glue.codes" class="link">GlueCodes Studio</a> now!</p>
<ul class="nav">
<li class="navItem">
<a href="https://www.facebook.com/groups/gluecodesstudio" class="navLink"><i class="fab fa-facebook"></i></a>
</li>
<li class="navItem">
<a href="https://www.youtube.com/channel/UCDtO8rCRAYyzM6pRXy39__A/featured?view_as=subscriber" class="navLink"><i class="fab fa-youtube"></i></a>
</li>
<li class="navItem">
<a href="https://www.linkedin.com/company/gluecodes" class="navLink"><i class="fab fa-linkedin-in"></i></a>
</li>
</ul>
</footer>
reusableSlots/header
:
<nav class="navbar navbar-light bg-light wrapper">
<a class="navbar-brand link" href="/">
<img-x width="30" height="30" alt="FoodAdvisor" class="logo">
<script>
props.src = mediaFiles['logo.png'].src
</script>
</img-x> FoodAdvisor
</a>
</nav>
You can access any images or videos you drop in the studio via
mediaFiles
variable which is an object where file names are the keys. Implicitly there is a Webpack Responsive Loader involved which gives yousrc
andplaceholder
.
Styles
For styling, although it feels like coding oldschool HTML and CSS, you'll be implicitly using CSS Modules. GlueCodes Studio gives you a beautiful balance between scoped and global styling. So, you can theme your app globally and at the same time style chosen parts of the UI in isolation. You'll simply be using CSS classes and because of the implicit scoping you can safely duplicate class names among different slots.
Notice a rather unusual
@import
statements. It's a way of importing third-party CSS from dependencies or global styles. The names must match the ones in Dependencies JSON or name of a global stylesheet.
pages/index/This Page CSS
@import 'bootstrap';
pages/index/Content Slot CSS
@import 'bootstrap';
@import 'fa';
@import 'theme';
.contentWrapper {
padding: 0 20px;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 30px;
margin-top: 40px;
}
.heading {
margin-bottom: 0;
font-size: 32px;
}
.thumbnail {
transition: transform 0.3s;
}
.thumbnail:hover {
transform: translateY(-4px);
}
.name {
font-weight: 700;
font-size: 16px;
color: rgb(25, 25, 25);
}
.category {
font-size: 13px;
color: #666;
}
reusableSlots/filters
:
.wrapper {
padding: 0 20px;
padding-top: 75px;
min-width: 250px;
}
.filterSet, .filterItem {
margin: 0;
padding: 0;
}
.filterSet {
margin-bottom: 30px;
}
.filterItem {
list-style: none;
}
.filterItem label {
cursor: pointer;
}
.label {
padding-left: 4px;
}
.heading {
padding-bottom: 15px;
font-weight: 700;
font-size: 16px;
color: rgb(25, 25, 25);
}
reusableSlots/footer
:
@import 'fa';
.wrapper {
margin-top: 70px;
padding: 20px;
background-color: #1C2023;
color: white;
}
.link {
color: white;
}
.link:hover {
color: #219F4D;
text-decoration: none;
}
.nav {
display: flex;
margin: 0;
padding: 0;
}
.navItem {
list-style: none;
}
.navLink {
display: inline-block;
margin-right: 2px;
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 18px;
border-radius: 50%;
background-color: #272a2e;
}
.navLink,
.navLink:hover,
.navLink:active,
.navLink.visited {
text-decoration: none;
color: white;
}
.navLink:hover {
background-color: #219F4D;
}
reusableSlots/header
:
.wrapper {
padding: 20px;
background: #1C2023;
margin-bottom: 30px;
}
.link {
color: white;
font-size: 18px;
font-weight: 700;
}
.link,
.link:hover,
.link:active,
.link:visited {
color: white;
text-decoration: none;
}
.logo {
margin-right: 3px;
}
What's next?
As you may have noticed there is a tone of details which hopefully is reasonably absorbable. I'll share a direct link to the project soon after releasing this article. Enjoy building your custom CMSs with GlueCodes Studio and Strapi.
Let me know whether I should write Part 2 or if there is some other integration you'd love to see.
Also, join our Facebook Forum
Top comments (0)