Hi everyone!๐
It's been a while since my last post, but I have a good reason: I was on vacations! Yeah! ๐โ๏ธ๐
And now it is time for a new Mint/Crystal recipe: we are going to build a Client/Server application using Mint for the frontend (this post) and Crystal for the backend (the next post)!
So let's start ... but first:
may I have a Crystal Mint Lemonade? โ๏ธ๐น๐
The Application
We are going to build an application that will list four (not 3, nor 5 but 4!) non-alcoholic summer drinks (as shown in the post 4 Refreshing Summer Drinks)
The Frontend
In this post we are going to build the frontend of our application ... uh oh! wait! We don't have a name for our application ... mmm let me think ... it will be called: Summer drinks! ๐น
And as we already mention, we are going to build the frontend using Mint!
We won't be doing a step-by-step tutorial but instead we are going to show the highlights of the source code.
The Structure
The project's structure is inspired by Mint Realworld and is the following:
mint-front
|
|- public
|- source
| |- components
| |- entities
| |- pages
| |- stores
| |- Main.mint
| |- Routes.mint
|
|- mint.json
Routing
Routing in Mint is really simple. For our application we need 3 routes:
mint-front/source/Routes.mint
routes {
/ {
Application.navigateTo(Page::Home)
}
/drinks {
parallel {
Application.navigateTo(Page::Drinks)
Stores.Drinks.load()
}
}
* {
Application.navigateTo(Page::NotFound)
}
}
What's important here is that when navigating to /drinks
:
- we start loading the drinks
- and in parallel we start rendering the view.
In the Application store
we are going to save the current page:
mint-front/source/stores/Application.mint
store Application {
state page : Page = Page::Home
fun navigateTo (page : Page) : Promise(Never, Void) {
sequence {
next { page = page}
Http.abortAll()
}
}
}
And the Main component will be responsible for rendering the correct view given the current page:
mint-front/source/Main.mint
component Main {
connect Application exposing { page }
fun render : Html {
<Layout>
case (page) {
Page::Home =>
<Pages.Home/>
Page::Drinks =>
<Pages.Drinks/>
Page::NotFound =>
<div>"Where am I?!"</div>
}
</Layout>
}
}
Entities
We will be working with just one entity: the Drink
itself! Here's the definition and the way to create an empty one:
mint-front/source/entities/Drink.mint
record Drink {
id : Number,
icon : String,
name : String,
url : String
}
module Drink {
fun empty : Drink {
{
id = 0,
icon = "",
name = "",
url = ""
}
}
}
Requesting the drinks
Here's an excerpt of the function #Stores.Drinks.load()
showing the request
we send to the server:
mint-front/source/stores/Drinks.mint
fun load() : Promise(Never, Void) {
sequence {
next { status = Stores.Status::Loading }
response = "https://demo5780178.mockable.io/drinks"
|> Http.get()
|> Http.header("Content-Type", "application/json")
|> Http.send()
newStatus = case (response.status) {
404 => Stores.Status::Error("Not Found")
=> try {
/* parse JSON */
object = Json.parse(response.body)
|> Maybe.toResult("")
/* JSON to Drinks */
drinks = decode object as Stores.Status.Drinks
Stores.Status::Ok(drinks)
} catch Object.Error => error {
Stores.Status::Error("Could not decode the response.")
} catch String => error {
Stores.Status::Error("Could not parse the response.")
}
}
next { status = newStatus }
...
In sequence
, we will:
- update the
status
toloading
. - send the
request
(waiting for the response). - define the
new status
given the response. If the response was successful then we try to parse thedrinks
in the response. - and finally, we change the
status
.
Another important element here is how we implement the different status
. We use enums like this:
enum Stores.Status(a) {
Initial
Loading
Error(String)
Ok(a)
}
Notice how easy is to send the request
and handle the response (parse
and decode
the JSON
data)! ๐ค
Listing the drinks (the Drinks
component)
This component will be responsible of showing the list of drinks. So first it needs to connect to the store
:
mint-front/source/components/Drinks.mint
component Drinks {
connect Stores.Drinks exposing { status }
...
}
Then the rendering depends on the current status (here we only show the cases Loading
and Ok
):
component Drinks {
connect Stores.Drinks exposing { status }
...
fun render : Html {
case (status) {
...
Stores.Status::Loading =>
<div::base>
<div::message>
"Loading drinks..."
</div>
</div>
...
Stores.Status::Ok =>
<div>
<{ drinksItems }>
</div>
}
}
}
drinkItems
and drinks
are computed properties that extract the data from the status:
get drinks : Array(Drink) {
case (status) {
Stores.Status::Ok data => data.drinks
=> []
}
}
get drinksItems : Array(Html) {
drinks
|> Array.map((drink : Drink) : Html { <Drinks.Item drink={drink}/> })
|> intersperse(<div::divider/>)
}
Notice that each drink
is rendered by the component Drinks.Item
.
The Full Client Application ๐ค๐น
Here is the source code of the recipe! And remember that we run the application using: ๐
$ mint-lang start
Mint - Running the development server
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Ensuring dependencies... 279ฮผs
โ Parsing files... 19.301ms
โ Development server started on http://127.0.0.1:3000/
Mocking the Backend
We still have not implemented the backend but we may use Mockable.io to mock it. Here is the response we need:
{
"drinks": [{
"id": 1,
"icon": "๐",
"name": "Strawberry Limeade",
"url": "https://www.youtube.com/watch?v=SqSZ8po1tmU"
}, {
"id": 2,
"icon": "โฑ",
"name": "Melon Sorbet Float",
"url": "https://www.youtube.com/watch?v=hcqMtASkn8U"
}, {
"id": 3,
"icon": "๐จ",
"name": "Raspberry Vanilla Soda",
"url": "https://www.youtube.com/watch?v=DkARNOFDnwA"
}, {
"id": 4,
"icon": "๐ด",
"name": "Cantaloupe Mint Agua Fresca",
"url": "https://www.youtube.com/watch?v=Zxz-DYSKcIk"
}]
}
Also notice that the request URL is hardcoded in mint-front/source/stores/Drinks.mint
๐
Farewell and see you later. Summing up.
We've reached the end of the recipe!๐จโ๐ณ We have implemented our second application in Mint๐:
- using
stores
for saving the state of our application (current page
anddrinks
) - using
enums
to implement the differentstatus
. - using
components
with conditional rendering (given thecurrent status
)
And remember that, in the next recipe, we will implement the server in Crystal! ๐ช๐ค
Hope you enjoyed it! Until next recipe!๐จโ๐ณ๐น
Photo by Jamie Street on Unsplash
Top comments (1)
Very nice post! I'm always psyched to see posts about Mint ๐