DEV Community

Kristian Pedersen
Kristian Pedersen

Posted on

#30daysofelm Day 23: Simple layout with elm-ui

This is day 23 of my 30 day Elm challenge

Try + view source: https://ellie-app.com/c2WWjMTM4ZLa1

Screenshow of three part layout

About today's project

I made a React project a few months back that I eventually want to remake in Elm: https://kristianpedersen.github.io/knights-tour-react/

Basically, you click a chess board position, and the program finds a valid sequence of moves where the knight visits every position on the board.

I'm happy with the concept and interactions, but the visuals need a lot of work.

Especially the board being positioned all the way to the left looks weird, and somehow in my caffeine-crazed coding bonanza, I managed to get the SVG lines to resize, but a "fix" stopped the board from resizing. :D

Also, the colors and borders make it look like it was made 15 years ago.

Elm-ui is much nicer to read

I touched upon this in my previous quick elm-ui encounter, but CSS can be difficult.

In my case, struggling with CSS is mostly my own fault for not bothering to learn it properly.

Still though, here's a centered div:

<html>
    <head>
    <style>
        .center-screen {
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            text-align: center;
            min-height: 100vh;
        }

    </style>
    </head>

    <body>
        <div class="center-screen">
            I'm in the center
        </div>
    </body>
 </html>
Enter fullscreen mode Exit fullscreen mode

From https://stackoverflow.com/questions/31217268/center-div-on-the-middle-of-screen

Here's my elm-ui program from day 7:

import Element exposing (..)


centeredDiv =
    column
        [ width (px 200)
        , height (px 200)
        , centerX
        , centerY
        ]
        [ el [ centerX, centerY ] (text "I am centered")
        ]


main =
    layout [] centeredDiv
Enter fullscreen mode Exit fullscreen mode

There's just a lot less noise in the second example. Now, if this is true in such a small example, imagine how this adds up.

Today's code

I'm still an elm-ui beginner. I had some problems initially that happened because I provided the wrong type annotations.

Once I removed them, the errors went away too, so today is an automatic type inference day.

Generic slider function

The code for making a slider in elm-ui is kind of long, to be honest.

My project needed two sliders, but I didn't want two huge slider functions, so I made one generalized slider function:

slider attributes =
    Input.slider
        [ height (px 30)
        , behindContent
            (el
                [ width fill
                , height (px 2)
                , centerY
                , Background.color (rgb255 255 255 255)
                , Border.rounded 2
                ]
                none
            )
        ]
        { onChange = attributes.message
        , label =
            Input.labelAbove []
                (text attributes.text)
        , min = attributes.min
        , max = attributes.max
        , step = Just 1
        , value = attributes.value
        , thumb =
            Input.defaultThumb
        }
Enter fullscreen mode Exit fullscreen mode

attributes is a record that contains the info I would want in a smaller slider function.

The sliders for the board size and animation speed now look nicer, instead of repeating the whole mess above twice:

sliderBoardSize model =
    el [ alignLeft ]
        (slider
            { min = 5
            , max = 26
            , value = model.boardSize
            , text = "Board size"
            , message = UpdateBoardSize
            }
        )


sliderAnimationRate model =
    el [ alignRight ]
        (slider
            { min = 5
            , max = 1000
            , value = model.animationSpeed
            , text = "Animation speed"
            , message = UpdateAnimationSpeed
            }
        )
Enter fullscreen mode Exit fullscreen mode

Panel for the controls

The two sliders then get included in a "nav bar", which looks like this:

controls model =
    row [ width fill, centerX, spacing 30, padding 30, Background.color (rgb255 200 200 100) ]
        [ sliderBoardSize model
        , sliderAnimationRate model
        ]
Enter fullscreen mode Exit fullscreen mode

What's there to say? It's a full-width row with two sliders in it. elm-ui code is just so nice to look at! <3

Info text

Then underneath the controls, I wanted some info text to be displayed:

infoText model =
    row
        [ width fill
        , centerX
        , spacing 30
        , padding 30
        , Background.color (rgb255 50 50 50)
        , Font.color (rgb255 200 200 200)
        ]
        [ el [ centerX ]
            (text
                ("The board size is "
                    ++ (model.boardSize |> Debug.toString)
                    ++ ", and the animation interval is "
                    ++ (model.animationSpeed |> Debug.toString)
                    ++ " ms"
                )
            )
        ]
Enter fullscreen mode Exit fullscreen mode

"Board"

Honestly, I just want to watch some Netflix right now, so I didn't bother doing the board properly with the letters and numbers. I'll do that tomorrow.

For now, it expands and contracts along with the board size slider, which I think is pretty cool. :)

showRow model =
    row [ width fill, height fill ] <|
        (List.range 1 (floor model.boardSize)
            |> List.map (\_ -> el [ width fill, centerY ] (text "Hi there"))
        )


showBoard model =
    column [ width fill, height fill, Font.center ]
        (List.range 1 (floor model.boardSize)
            |> List.map (\_ -> showRow model)
        )
Enter fullscreen mode Exit fullscreen mode

First, we create a row of 8 cells: ********
Then, we create a column with 8 of those:

********
********
********
********
********
********
********
********
Enter fullscreen mode Exit fullscreen mode

The main fullscreen container, and the view function

view model =
    Element.layout []
        (fullscreenContainer model)


fullscreenContainer model =
    column [ width fill, height fill, spacing 0, padding 0 ] <|
        List.map
            (\elmUiSection -> elmUiSection model)
            -- I want to add the "buttons" function to this list, but Elm won't let me because of a type mismatch
            [ controls, infoText, showBoard ]
Enter fullscreen mode Exit fullscreen mode

Element.layout is the topmost level in this elm-ui application. In my case, it just takes a column that contains everything.

I'm using map, because each of the elmUiSections, ([controls, infoText, showBoard]) take the model as their argument.

I could have also written this:

column [ width fill, height fill, spacing 0, padding 0 ] <|
    [ controls model, infoText model, showBoard model ]
Enter fullscreen mode Exit fullscreen mode

But that kind of needless repetition just puts me asleep.

The "buttons" function didn't work in this list. Lists can only consist of items of the same type, and the button is a different type because of its onPress message.

Main, model, update, the usual stuff

module Main exposing (..)

import Browser
import Element exposing (Element, alignLeft, alignRight, behindContent, centerX, centerY, column, el, fill, height, none, padding, px, rgb255, row, spacing, text, width)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input


main : Program () Model Msg
main =
    Browser.sandbox
        { init = init
        , view = view
        , update = update
        }


type alias Model =
    { boardSize : Float
    , animationSpeed : Float
    }


init : Model
init =
    { boardSize = 8, animationSpeed = 50 }


type Msg
    = UpdateBoardSize Float
    | UpdateAnimationSpeed Float
    | BoardSizeFromButton Int


update : Msg -> Model -> Model
update msg model =
    case msg of
        UpdateBoardSize v ->
            { model | boardSize = v }

        UpdateAnimationSpeed v ->
            { model | animationSpeed = v }

        BoardSizeFromButton v ->
            { model | boardSize = toFloat v }
Enter fullscreen mode Exit fullscreen mode

This is a very standard Elm setup. I'd definitely change the Msg names, as they're not really clear. SliderBoardSize, SliderAnimationSpeed and ButtonBoardSize would be better names for me.

Also, almost at the top, rather than exposing so many individual things from Element, I think I would just expose all.

Conclusion

This was my 2nd time using elm-ui. Type annotations gave me some trouble to begin with, but other than that, I must say that it feels really intuitive.

I'm just going to be honest and say that it would have taken me a lot more effort to make today's project in CSS.

My CSS skills aren't good, so I use elm-ui, and my React code gives me runtime errors, so I use elm.

I'll definitely look more into elm-ui for future projects - it's really nice.

Oldest comments (1)

Collapse
 
sirseanofloxley profile image
Sean Allin Newell

Awesome! Keep going! I've loved using elm-ui since I found it last year.