KnitZilla
Simon Bundgaard-Egeberg
Posted on August 22, 2019
Writing a Knitting app for my wife!
DISCLAIMER: THIS IS WRITTEN WITHOUT HOOKS
Two weeks ago I released a post about what reason is as a language with a promise that I would show a real world application using that technology. here
Application: knitzilla
(I am using a free dyno on heroku, so the first attempt to open will be a little slow).
Source code here
First off, I want to admit that I kinda "hacked" the beginning of this app. To avoid setting up a deployment script with webpack and service workers and such, I chose to try the "Reason is easily pluggable to existing JavaScript apps" selling point. And I must admit, I was not disappointed.
import React from "react";
import ReactDOM from "react-dom";
// This is a reason component
import App from "./BackgroundWrapper.bs";
import * as serviceWorker from "./serviceWorker";
import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider";
import theme from "./theme";
import CssBaseline from "@material-ui/core/CssBaseline/CssBaseline";
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<CssBaseline />
{/*Here we are rendering the reason component */}
<App />
</MuiThemeProvider>, document.getElementById("root"));
serviceWorker.register();
So I am using a JavaScript API to create my Material UI theme and also to register the application root.
Everything from <App/>
and down are pure, reasonably written code.
In Reason land now I did several things which I think is cool to cover here:
- Routing (built-in)
- Write and Read from browser localStorage
- Typesafe css-in-js (bs-css).
Routing
Since pattern matching is amazing, things as routing suddenly becomes a trivial matter
type routes = | Home | Calc;
type state = {
activeRoute: routes,
startClientX: int,
};
/* binding to the route works quite like hooks does,
meaning you can subscribe and unsubscribe to events in the same function */
let didMount = self => {
let touchListen = ReasonReact.Router.watchUrl(url =>
switch (url.path) {
| ["calc"] => self.send(Route(Calc))
| _ => self.send(Route(Home))
}
);
self.onUnmount(() => ReasonReact.Router.unwatchUrl(touchListen));
},
/* Somewhere in rendering logic */
<div>
{switch (self.state.activeRoute) {
| Home => <App />
| Calc => <RowCalcRoot />
}}
</div>
If you open the app or code, you will notice that I have implemented a "swipe" feature as well, these are bound in the same life-cycle method as shown above, but left out for brevity.
Read and write JSON in reason
When we write in JavaScript, we are used to basically a zero overhead when reading or writing JSON objects to and from JavaScript.
In most other languages this needs a little bit of work. Now there are libraries out there to help with JSON parsing in Reason, but I took a more native approach to learn the language better.
module RowCalcParser = {
type rowCalc = {
title: string,
rows: int,
};
type rows = array(rowCalc);in e
let decodeRow = jsonString => {
Json.Decode.{
title: jsonString |> field("title", string),
rows: jsonString |> field("rows", int),
};
};
let decode = jsonString => Json.Decode.(jsonString |> array(decodeRow));
let encodeRow = row => {
Json.Encode.(
object_([("title", string(row.title)), ("rows", int(row.rows))])
);
};
let encode = (rows: array(rowCalc)) => {
Json.Encode.(rows |> array(encodeRow));
};
};
Since Reason needs to have 100% type coverage, we need to annonate how to parse earch part of the JSON object. I have made a Row Calculator parser to persist and hydrate the app with an array of rowCalc
types.
In the code i use the decode and encode functions, and these uses the rowEncoders/decoders to to help parse the objects in the array.
The usage of the encoder is shown as:
Dom.Storage.(
localStorage
|> setItem( "rowCalcs",
newSelf.state.rows |> RowCalcParser.encode |> Json.stringify)
);
The syntax above might look weird. After we haved talked through it, you will find it awesome is I do(I hope).
What we are saying that in the Dom.Storage namespace we want to use localStorage, localStorage is passed into the first argument of setItem
, setItem
like in JavaScript takes two strings, key and value.
The key being the string rowCalc, and the values being the state.rows from the component, these rows, which is a reason list type is passed into the encode function as shown above, which in turn can be stringified and saved. Just like this, when decoding them it is just the opposite.
BS-CSS
anyone who have used css-in-jss will agree that it provides some really powerfull features to the whole css experience. Though when using css-in-jss and you need to pass for instance 100% you have to do this a string, like borderRadius: "100%"
you will have no help what so ever to tell you if 100% is even an option.
BS-CSS takes css-in-jss to a new level as shown below.
module Styles = {
open Css;
let floatingRightSideBack =
style([position(absolute), top(`percent(50.0)), left(px(5))]);
let numberWrapper =
style([
border(px(1), solid, rgba(0, 0, 0, 0.87)),
borderRadius(px(5)),
padding2(~v=px(12), ~h=px(24)),
]);
let formControl =
style([
position(absolute),
bottom(`percent(5.0)),
right(`percent(50.0)),
transform(translateX(`percent(50.0))),
]);
let counterContainer =
style([
width(`percent(80.0)),
overflowY(auto),
height(`percent(60.0)),
]);
};
Notice how when using sizes, we define the kind, being percent, px or rem's with a size in them. Some values take floats, like percent above, and some only integers like px.
Notice in the numberWrapper on the padding2 key, here you define the vertical padding noted with ~v
and horizontal with ~h
, so no more mixing these two up, pretty neat!
Conclusion
Even though KnitZilla is written with the now legacy api of ReasonReact it still shows the benefits of writing web with a fully typed language.
Moreover there are features in Reason that you will forever miss in JavaScript, like types (du'h) and pattern matching with the switch.
If you want to learn more about reason and ReasonReact visit:
ReasonReact
Reason
Posted on August 22, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.