Last week I mentioned I had a little itch I wanted to scratch. I picked elm as the tool to scratch it with. Then, instead of doing just that, I spent the whole evening whining about my onboarding experience.
"Whaah! Whaah! People who made this amazing language and documentation left a little bit of dirt on the red carpet they unrolled for me! Whaah! Whaah!"
Now that my tears are all dried up, it's time to do some actual programming. So, back to scratching my itch.
Font Awesome
Font Awesome is a popular free icon font. I use it often in business-y projects without firm design guidelines. It used to be a breeze to just bounce over to their site and lookup the glyph I needed.
But lately, their site has really gone downhill. Just look at this shit.
Ads, upsells, popups.
Pages are huge, taking about 10 seconds on average to load. The "subscribe to our mailing list" text input is "conveniently" placed above the search field, so it's easy to confuse. If I spend too long dicking around, I'll be interrupted by popups. "Are you suuure you don't want to subscribe to our spam list?"
Even when I find the icon I want, I can't just copy it and be done. Instead, I have to click on it, wait 10-15 seconds for another ad-laden page to be loaded, and then I am finally allowed to leave.
All I need is a place where I can see the list of Font Awesome icons, search by name and quickly copy the var name.
So I am about to make one.
Setup
The official elm guide and tooling are optimized for learning the language. They don't seem to suggest any concrete setup or boilerplate for making the actual apps.
A quick round of googling led me to Elm webpack starter. Decent number of stars, easy npm installation, webpack build system and live reload. Sounds good to me.
After going through their tutorial, I was indeed left with a functioning elm mini-site I could customize.
Then, I customized:
- I cleaned up
package.json
andelm-package.json
. Changed all the versions, descriptions, repos and licenses to be for my own project. - In
index.html
, I updated the meta tags and title. - I replaced their
favicon.ico
with my ownfavicon.png
and updated theindex.html
- Added my LICENCE file (generated by github)
- In
index.js
, I removed bootstrap.js and jQuery imports, as I will not be using them - Cleaned up
README.md
, added my own content
I mention all this as a reminder. Details are important. You don't want your project to look like a quick hack inside someone else's project.
Next, I looked into the main.scss
.
$icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/_mixins.scss';
@import '~bootstrap-sass/assets/stylesheets/_bootstrap.scss';
// can add Boostrap overrides, additional Sass/CSS below...
Boilerplate was importing the entire bootstrap stylesheet, including its glyphicons font. I decided to keep bootstrap, but I will not need glyphicons. We are all about font awesome here.
The quickest solution would be to fudge the $icon-font-path
variable, but that would create icky errors. Instead, I dug into bootstrap's sass sources and imported just the styles I needed (hint: "~
" will be resolved by webpack as path to node_modules
).
// Core variables and mixins
@import "~bootstrap-sass/assets/stylesheets/bootstrap/_variables.scss";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/_mixins.scss";
// Reset and dependencies
@import "~bootstrap-sass/assets/stylesheets/bootstrap/_normalize.scss";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/_print.scss";
//@import "~bootstrap-sass/assets/stylesheets/bootstrap/_glyphicons.scss";
// ... more stuff here, copied over from the original bootstrap.scss
This allowed me to exclude some of the bootstrap styles I won't use in this tiny project, while leaving an open door to bring them back later if needed.
Next, I brought in Font Awesome.
npm install --save-dev font-awesome
Added the reference into main.scss
:
$fa-font-path: "~font-awesome/fonts/";
@import '~font-awesome/scss/font-awesome.scss';
Webpack needed to be told what to do with font references. In webpack.config.js
:
if (TARGET_ENV === 'development') {
//...
module.exports = merge(commonConfig, {
//...
module: {
loaders: [
//...
{
test: /\.(woff|woff2|ttf|eot|svg)(\?v=[a-z0-9]\.[a-z0-9]\.[a-z0-9])?$/,
loader: 'url-loader?limit=100000'
}
]
}
});
}
This should allow me to use font awesome classes (eg "fa fa-times"
) to display its glyphs on the screen.
One final bit remained. Where do I get the full list of font awesome class names for my app?
I guess I could just hard-code it into the app, but that felt dirty and too labor intensive. I don't want to keep having to come back to update this project. I dug around font awesome's module directory. The most convenient place where this list appears is inside the font-awesome/scss/_variables.scss
file.
//...
$fa-var-500px: "\f26e";
$fa-var-address-book: "\f2b9";
$fa-var-address-book-o: "\f2ba";
$fa-var-address-card: "\f2bb";
//...
So this is where I'll load it from.
I quickly zeroed in on sass-to-js-var-loader. This webpack plugin will read the variables from a scss file, camelize them (unfortunate for me, but workable) and provide them as a javascript hash.
var faVars = require('!sass-to-js-var!../../node_modules/font-awesome/scss/_variables.scss');
var faNames = Object.keys(faVars);
// [..., 'faVar500px', 'faVarAddressBook', ...]
Thus, I had a list of all font awesome variables, including glyph names, in an inconvenient camelized form.
It was tempting to keep massaging these variables right here, but I refrained. Let's see how elm deals with this.
Elm overview
I'll use the existing boilerplate to do a quick rundown of elm features, as I understand them at this point.
Disclaimer - I am a noob. This is my first project in elm. I learn as I write this.
module Main exposing (..)
Module declaration must be at the top. Name of the module must be Pascal Case and match the directory structure. exposing
is the list of symbols I am exporting for other modules. (..)
means I am exposing everything. Pretty standard stuff.
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing ( onClick )
These are the modules I am importing. exposing
here means which names will be dumped into the global scope. If I didn't do exposing (onClick)
, I'd have to write Html.Events.onClick
every time I wanted to add a click handler.
import Components.Hello exposing ( hello )
This is importing a local component from Components/Hello.elm
. The syntax is the same as for "official" built in modules, as well as for modules from elm's version of npm.
Overall, pretty neat little module system.
-- APP
main : Program Never Int Msg
main =
Html.beginnerProgram { model = model, view = view, update = update }
This is the "entry point". When I do Elm.Main.embed(document.getElementById('main');
in index.js
, the elm runtime will look up this main
function and run that.
Yes, this is a function declaration. It has two rows. The upper row is for types (you can tell they are types because of PascalCase). The lower row is for arguments and actual code (which is traditionally moved into the next line and indented).
This particular function looks like it takes 4 arguments, but it actually doesn't take any. Instead, it just returns a value of type Program Never Int Msg
. I think this is like a resolved generic type, where generic Program
is built into the language and the other 3 are defined by the user.
Html.beginnerProgram
is a helper function which creates a react-like web application. This thing with curly braces is its single argument. It's the Record type, similar to hashes / dictionaries / javascript objects.
There is no return
statement. Every function body is an expression that always returns something.
-- MODEL
type alias Model = Int
model : number
model = 0
type alias
is, like the name says, alias for a type. In this case, every time I say Model
, it's the same as if I said Int
(integer). This is more useful when applied to records, like the one in the main
code block.
model
is a variable. As the name implies, it is the central data store of the app (like the store in redux). I am not sure why is it declared as number
instead of Model
, though.
-- UPDATE
type Msg = NoOp | Increment
update : Msg -> Model -> Model
update msg model =
case msg of
NoOp -> model
Increment -> model + 1
Msg
is the equivalent of "action creator" in react. It's what I send into the elm runtime to trigger changes in my app.
It is declared as being either NoOp
or Increment
. These are the equivalent to symbols in Ruby or values of an enum.
update
function takes 2 arguments - Msg
and Model
, and returns a Model
. Yup, it's the reducer from redux.
Notice the arrows (->
)? That's how you separate arguments in function declarations (it has to do with currying). When you write the implementation part (update msg model =
), you just use spaces as separators. Not the happiest syntax choice, in my opinion.
case
is like the higher level functional version of the "normal" switch/case
syntax. It's pretty clear what's going on here. Depending of the Msg
that gets sent in, I either do nothing or increment the model value.
-- VIEW
-- Html is defined as: elem [ attribs ][ children ]
-- CSS can be applied via class names or inline style attrib
view : Model -> Html Msg
view model =
div [ class "container", style [("margin-top", "30px"), ( "text-align", "center" )] ][ -- inline CSS (literal)
div [ class "row" ][
div [ class "col-xs-12" ][
div [ class "jumbotron" ][
img [ src "static/img/elm.jpg", style styles.img ] [] -- inline CSS (via var)
, hello model -- ext 'hello' component (takes 'model' as arg)
, p [] [ text ( "Elm Webpack Starter" ) ]
, button [ class "btn btn-primary btn-lg", onClick Increment ] [ -- click handler
span[ class "glyphicon glyphicon-star" ][] -- glyphicon
, span[][ text "FTW!" ]
]
]
]
]
]
This final part is obviously the equivalent of render()
in react. It's a function that takes the Model
(aka props
) and returns the Html Msg
. Elm will use Html
part to render the DOM and Msg
is the type of messages which will be raised by any events.
For example:
button [ class "btn btn-primary btn-lg", onClick Increment ]
This onClick Increment
attribute will emit Increment
action when user clicks the button. Elm will then call the update
function with Increment
message, which will increment the model
variable, causing re-render of the view
.
All these ugly square brackets are actually arrays. This is how elm represents its shadow DOM. Things like button
, div
, class
and text
are functions. div
takes 2 arguments: an array of attributes and an array of children elements. class
and text
take a single string argument. It's all actually pretty understandable once you get used to it.
-- hello component
hello : Int -> Html a
hello model =
div
[ class "h1" ]
[ text ( "Hello, Elm" ++ ( "!" |> String.repeat model ) ) ]
Model is rendered inside the Hello
component.
Html a
is like a generic version of Html Msg
, where a
can be any type (it's done like this because this component doesn't "emit" anything). Note the lowercase.
++
is for string concatenation.
|>
is called pipe. It's a fancy way to write String.repeat model "!"
. It just allows me to chain multiple functions together and avoid the "brace hell" from other functional languages.
Notice that repeat
is called from the String
module instead directly on the "!"
string variable. That's just the functional way of doing things. You never mix data and functionality together.
There are a few more scattered pieces, but this is the core of it. Program
, model
, update
and view
. Building blocks of an elm web app.
Showing the icons
Now that I've familiarized myself with this boilerplate code, I'll delete it and write some of my own.
First order of business - get list of icons into the app and show it on screen.
There are several ways to do interop between javascript and elm. One immediately sticks out to me - "flags".
You can think of this as some static configuration for your Elm program.
Exactly what I need.
I'll start by writing a new entry point inside the Main.elm
file.
-- APP
main : Program (List String) Model Msg
main =
Html.programWithFlags {
view = view,
update = update,
init = init,
subscriptions = subscriptions
}
This is similar stuff as before, except I am using a different helper. programWithFlags
will allow me to accept initialization data passed from the javascript world.
-- MODEL
type alias Model = {
icons: List Icon
}
init : List String -> (Model, Cmd Msg)
init rawKeys = (
{
icons = loadIcons rawKeys
},
Cmd.none
)
My new model will be a list of Icon
-s. init
is the replacement for the old model
variable. It takes some flags from javascript (in this case, a list of strings named rawKeys
), and returns my initial model and command.
I will define Icon
and loadIcons
a bit later.
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
Here is where I would handle ajax responses and other async stuff coming in from the outside. I don't need any of that, but I haven't found a way to tell elm that. So instead, I created this useless stub.
-- UPDATE
type Msg = NoOp
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
NoOp -> (model, Cmd.none)
Update function is extended with this Cmd
stuff. This is like a redux thunk, an async action you send into elm, and the results later pop in through the subscriptions
.
(Model, Cmd Msg)
construct is called "tuple". It's kind of like a record without key names. Or a fixed-length variable-type list, I guess. It's the third and final mass data carrier type in elm (the other two being lists and records).
Either way, all this is just stubbed for now. I don't need it yet.
-- VIEW
view : Model -> Html Msg
view model =
div [ class "container" ] [
div [ class "row" ] [
div [ class "col-xs-12" ] [
ul [] (
List.map (\icon -> li [] [ renderIcon icon ]) model.icons
)
]
]
]
The view just calls renderIcon
for each icon, and shoves the results into a <ul>
.
So what about all this missing icon stuff?
I decided to compartmentalize everything to do with icons (loading, types and rendering) into Components/Icon.elm
. Is this the right architecture? No idea. But it seems to serve my purposes.
At the top of Main.elm
, I added:
import Components.Icon exposing ( Icon, loadIcons, renderIcon )
I deleted the old Hello component. Then, in Components/Icon.elm
:
type alias Icon = {
tokens: List String,
name: String,
class: String
}
I can probably get away with just the string, but I want to give myself some space for growth.
renderIcon : Icon -> Html a
renderIcon icon =
span [] [
i [ class ("fa " ++ icon.class) ] [],
span [] [
text icon.class
]
]
Render is pretty straightforward for now. I just want to show the glyph and its name.
Loading icons is the most complicated part so far. As a reminder, the raw data will come in this form:
[
"faVar500px",
"faVaraddressBook",
"faVaraddressBookO",
"faVaraddressCard",
"faVaraddressCardO",
...
]
So how do I chew through that using elm?
Like this:
loadIcons : List String -> List Icon
loadIcons rawKeys =
rawKeys
|> List.filter (String.startsWith "faVar")
|> List.map (\rawKey ->
let
rawTokens = String.dropLeft 5 rawKey
matches = Regex.find Regex.All (Regex.regex "([A-Z0-9][a-z0-9]*)") rawTokens
tokens = List.map (\m -> String.toLower m.match) matches
in
{ tokens = tokens
, name = String.join "_" tokens
, class = "fa-" ++ String.join "-" tokens
}
)
First thing to note is the pipeline I spoke of earlier (|>
). I filter out all the values that don't start with "faVar"
(eg. faFontPath
), then map the results.
Then there's this let
/ in
construct. As far as I can tell, that's just a way to write linear code in elm. You setup some temporary values inside the let
, then return the result as usual from within the in
.
Inside let
, I cut off the faVar
part, then find all the words, then map them into lowercase tokens. To figure out how to do all this, I basically just googled elm's string and regex docs.
The in
block then uses these generated tokens to create an Icon
record for each input item.
It was all actually pretty straightforward. No javascript required.
All that remains is to connect the webpack provided icon list with elm using the second argument in the embed()
call.
var faVars = require('!sass-to-js-var!../../node_modules/font-awesome/scss/_variables.scss');
var faNames = Object.keys(faVars);
var Elm = require('../elm/Main');
Elm.Main.embed(document.getElementById('main'), faNames);
Execute npm run start
, and...
Full code up to this point is available here.
Filtering
Time to put that update loop to work.
type alias Model = {
icons: List Icon,
filter: String
}
I start by expanding model to hold the filter field.
-- UPDATE
type Msg =
SetFilter String
SetFilter message takes a single String parameter. This is one of those strange elm complex types that I don't know how to call.
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
SetFilter filter ->
({ model | filter = filter }, Cmd.none)
update
function handles `SetFilter
message by updating the model. This thing with the pipe (|
) character is the way you "update" records in elm. Well, you actually clone and modify them, this being functional programming and all. It's the equivalent to Object.assign({}, model, { filter: filter })
in javascript.
-- VIEW
view : Model -> Html Msg
view model =
div [] [
div [ class "navbar navbar-default navbar-static-top" ] [
div [ class "container" ] [
div [ class "navbar-header"] [
span [ class "navbar-brand" ] [
text "Font Awesomelm"
]
],
Html.form [ class "navbar-form navbar-right filter-form" ] [
div [ class "form-group" ] [
input [ placeholder "Filter", onInput SetFilter, class "form-control"] []
]
]
]
],
div [ class "container" ] [
div [ class "row" ] [
div [ class "col-xs-12" ] [
renderIconList model.icons model.filter
]
]
]
]
This render function got bootstrap-bloated real fast. I am already itching to refactor it into its own component.
This is the most important part: input [onInput SetFilter] []
. It will render an input field, which will emit the SetFilter <newValue>
action on every change. I have no idea how I'd do advanced stuff, with event object, bubbling etc. using this pattern.
renderIconList
is the new function I've defined in my Icon.elm
module:
renderIconList: List Icon -> String -> Html a
renderIconList icons filter =
let
filterRegex = Regex.regex filter
items = icons
|> List.filter (\icon -> Regex.contains filterRegex icon.name)
|> List.map (\icon -> li [] [ renderIcon icon ])
in
ul [] items
The only addition is the filterRegex
stuff, which filters the list based on the current filter.
Full code at this point is available here.
The debugger
While trying to get that regex working, I gave the elm debugger another look.
Every time the model gets updated, there is a new entry in this list. I can click on any entry and examine the complete state of the model at that point in time. I can also "rewind" the state back and forth at will. And that's all cool.
I notice something missing, though. Where is my filtered list of icons that reflects the current filter string? How do I see all those temp variables and regexes trapped inside the let
blocks?
Elm's docs are somewhat silent regarding this. Elm does have a Debug module, which used to have a watch functionality. But at some point that got removed. The only remaining facility is good old Debug.log
.
items = Debug.log "items" (icons
|> List.filter (\icon -> Regex.contains filterRegex icon.name)
|> List.map (\icon -> li [] [ renderIcon icon ]))
It's called "poor man's debugger" for a reason.
Here's my conundrum, then. The only state I can debug is the "official" model that gets declared with the elm runtime. But what if I have a derived state, like my filtered list? Should I keep that inside the model as well? That seems like a terrible practice. The items in my master list and filtered list are bound to get out of sync at some point.
Unless I am missing some obvious feature, outside of the flashy time-travelling gimmick, elm's debugging story seems very incomplete at this point.
Clipboard
The final problem I have to tackle is how do I copy the icon class into the clipboard.
Elm used to have a clipboard module, but it hasn't been updated in a year, and no longer works in the current elm build. So I'll just make my own clipboard handler using elm's ports system.
Ports allows elm to use javascript to do some heavy lifting outside of its sparkly clean world. The communication between the two is an asynchronous message bus. Elm emits a Cmd
action out into the real world, where it is received and handled by a javascript function. At some point later, javascript drops its response back into the elm Matrix, where it is caught by elm's subscribe
handler.
I will only use the elm -> javascript interop in this project.
port module Main exposing (..)
I start by adding this port
thing before my Main
module declaration. That's just an arbitrary syntax you have to add for a module to be exposed to the outside.
-- PORTS
port copyToClipboard : String -> Cmd msg
Port itself is declared as a function that takes some args (these will later appear in javascript world) and produces this generic Cmd
thing. I don't define any body here, all I need is this declaration.
type Msg =
SetFilter String
| CopyToClipboard String
I updated my Msg
type with the new CopyToClipboard
message type, which will be emitted from my Icon
component when user... Wait. How do I get this Msg
type into the Icon.elm
file? I can't just reference Main
, as that will create a circular dependency.
If you recall earlier, I hand-waved this concern away because my Icons didn't need to emit any events. Well, now they do.
renderIcon : Icon -> Html a -- <- Instead of "a", I need the Msg from Main here
Real elm apps use some kind of staggered architecture, where each Component (Container?) has its own model/cmd/view stack, and the Main then just combines them all together and distributes the messages downwards. But that seemed like an overkill for this tiny project.
So instead, I extracted Msg
definition into its own file and referenced it from both the Main and Icon modules.
-- File: Components/Icon.elm
import Msg exposing (..)
-- ... old stuff ...
renderIcon : Icon -> Html Msg
renderIcon icon =
span [ class ("icon-" ++ icon.class) ] [
i [ class ("fa " ++ icon.class) ] [],
span [ onClick (CopyToClipboard icon.class) ] [
text icon.class
]
]
On click, I emit the new Cmd
type and pass along the icon's class name as parameter. I also added a class name which I'll use to later locate this icon's DOM element.
-- UPDATE
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
SetFilter filter ->
({ model | filter = filter }, Cmd.none)
CopyToClipboard name ->
(model, copyToClipboard name)
Back in Main.elm
, the update function now knows how to handle the CopyToClipboard
event emitted by the icons. It calls copyToClipboard
port and passes in the icon class name.
//...
var app = Elm.Main.embed(document.getElementById('main'), faNames);
app.ports.copyToClipboard.subscribe(function (iconName) {
var element = document.querySelector('.icon-' + iconName);
var range = document.createRange();
range.selectNode(element);
window.getSelection().addRange(range);
document.execCommand('copy');
});
The final bit in the javascript land, to tie it all together.
Clicking on icon names will now select them and copy the class name to clipboard.
Full code at this point is available here.
Polish
I'll now quickly go through the final fixes and polish I had to do to get this project into a shippable state.
More reliable loading code
The old icon list loader had a problem. It would fail to properly decamelize tokens with leading digits. For example, $fa-var-battery-4
and $fa-var-css3
got converted into faVarBattery4
and faVarCss3
respectively. There is obviously no reliable way to convert these back into class names. Oops.
Instead of messing with crappy decamelization, I now just load _variables.scss
directly into a string using the raw-loader plugin, and then pass that into elm.
var faVars = require('!raw!../../node_modules/font-awesome/scss/_variables.scss');
var Elm = require('../elm/Main');
var app = Elm.Main.embed(document.getElementById('main'), faVars);
loadIcons : String -> List Icon
loadIcons faVarsTxt =
faVarsTxt
|> String.lines
|> List.filter (String.startsWith "$fa-var-")
|> List.map (\rawLine ->
let
columnIndex = Maybe.withDefault -1 (String.indexes ":" rawLine |> List.head)
dashedName = String.slice 8 columnIndex rawLine
in
{ name = Regex.replace Regex.All (Regex.regex "\\-") (\_ -> "_") dashedName
, class = "fa-" ++ dashedName
}
)
One tiny new thing here. Maybe
is elm's version of Nullable
. Elm docs are quick to point out how this is waaaay more safer and better, blah blah blah. Fine, it's a null
with a safety helmet.
Maybe.withDefault
will basically "unmaybe" a Maybe
.
The Icon
record has also changed.
type alias Icon = {
name: String,
class: String
}
I no longer keep the list of tokens, which I never ended up using. All I need is the underscored name and the class name.
And why do I need an underscored name, you might ask? I just do. I use it in my other projects, so it's convenient for me to have it listed. Perks of making your own tools.
Appearance
I did some boring scss stuff to make the app look more presentable.
No code, because who wants to read CSS code? You can see it in the final "main.scss" file, if you're curious.
Better filter
I now break user input into tokens and reform them so they'll better match the icon name.
renderIconList: List Icon -> String -> Html Msg
renderIconList icons filter =
let
filterTokens = Regex.split Regex.All (Regex.regex "[ _\\-.]+") filter
filterRegex = Regex.regex (String.join "_" filterTokens)
items = icons
|> List.filter (\icon -> Regex.contains filterRegex icon.name)
|> List.map (\icon -> li [] [ renderIcon icon ])
in
ul [ class "icons" ] items
It's not exactly fuzzy search, but every little bit helps. In some future version, I might see about adding some kind of synonym search, as some of font awesome names are a bit wonky.
Deployment
I'll briefly outline the process of deploying this thing to google pages.
I had to fix up a few niggles in webpack.config.js
first.
var commonConfig = {
output: {
path: outputPath,
filename: `static/js/${outputFilename}`,
I removed the leading slash from /static/js/${outputFilename}
, as that was killing the link to the generated js file.
{
test: /\.(woff|woff2|ttf|eot|svg)(\?v=[a-z0-9]\.[a-z0-9]\.[a-z0-9])?$/,
loader: 'url?limit=10000&name=[name].[ext]?[hash]'
}
This loader value inside the production section needed a name, with a [hash]
parameter.
plugins: [
new CopyWebpackPlugin([
{
from: 'src/favicon.png'
},
{
from: 'src/CNAME'
}
]),
src/CNAME
is where I keep my github pages domain name. This needs to be copied to dist, so it will be included inside gh-pages
branch.
I also nuked the boilerplate image that somehow survived the initial purge.
At this point, I ran npm run build
, and got a nice little static site generated inside the dist
folder.
Next, I downloaded this script and plopped it into my project. It will allow me to deploy the generated files to github pages without having to keep them in my repository.
I didn't need to configure anything. Just chmod +x
-ed it, ran it and it did the right thing. Font Awesomelm was available on https://github.com/panta82/font-awesomelm.
One final touch. I set up a CNAME redirect from one of my domains, so the project's final destination is fa.pantas.net (code).
Conclusion
I thought I would find functional programming onerous to use in an actual project. I actually found it quite easy once I got used to it. Data munching tasks are a breeze. Coming from React background, web framework concepts and flow came very naturally. I didn't do much async work, but I don't forsee myself having any issues with it.
Type safety was about what I expected. Kind of slow and fiddly to get going, but like eating vegetables, good for you in the long run. It certainly made refactoring much easier than in a test-less javascript.
On the downside, the debugger was kind of disappointing. You can't really debug everything, like you can in javascript. You are limited to the state inside one blessed "model" variable and the messages going in and out of the system. If you have a lot of derived state outside of the model, better get used to console.log
-ing.
That clipboard thing was worrying too. The one module I tried to use turned out to be dead, with no replacement in sight.
The ports system is an inviting escape hatch. But what happens if you start using it all the time? Will ambitious applications eventually deteriorate into a mess of interlinked javascript and elm code that no one dares to touch? I don't know.
The biggest hurdle is, as always, functional programming itself. Most frontend developers I know will not be too eager to leave their html tags and jQueries for those ugly nested arrays and strange pattern matching functions. Even react, with its inviting-looking JSX and plain javascript, is a struggle to get adopted.
Web developers are a tiny bunch of math nerds, a handful of designery people and a whole lot of mediocre shmoes. An esoteric functional language championed by the math nerds will not win over this space.
Final verdict: Elm is a nice place to visit, but I don't think I'll be moving here (just yet).