insurello / elm-ui-explorer / UiExplorer

Create an app that lets you browse and interact with UI you've created.

example-image In this image, the panel to the left is called the sidebar and the page selected in it is shown in the remaining space to the right.

Note that this package is built primarily for UI created with mdgriffith/elm-ui. You can still use elm/html with Element.html though.

Application

application : ApplicationConfig (Msg pageMsg) flags -> PageBuilder pageModel pageMsg flags -> Platform.Program Json.Decode.Value (Model pageModel flags) (Msg pageMsg)

Here we create our UI explorer app.

import MyCoolUi
import UiExplorer

pages =
    UiExplorer.firstPage
        "Button"
        (UiExplorer.static MyCoolUi.button)
        |> UiExplorer.nextPage
            "Footer"
            (UiExplorer.static MyCoolUi.footer)
        |> UiExplorer.nextPage
            "Login Form"
            { init = MyCoolUi.loginInit
            , update = MyCoolUi.loginUpdate
            , view =
                \pageSize model ->
                    MyCoolUi.loginView model
            , subscriptions = always Sub.none
            }

main =
    UiExplorer.application
        UiExplorer.defaultConfig
        pages

Note that we didn't add type signatures for pages and main in the example. If we did, we'd have to update it every time we add a new page and the type signatures would get messy. Instead it's best to just let the compiler infer it automatically.

defaultConfig : ApplicationConfig msg ()

The default application configuration.

{ flagsDecoder = Decode.succeed ()
, layoutOptions = []
, layoutAttributes = []
, relativeUrlPath = []
, title = Element.text "UI explorer"
}


type alias ApplicationConfig msg flags =
{ flagsDecoder : Json.Decode.Decoder flags
, layoutOptions : List Element.Option
, layoutAttributes : List (Element.Attribute msg)
, relativeUrlPath : List String
, sidebarTitle : Element msg 
}

These are settings we can change when creating our UI explorer application.


type Model pageModel flags


type Msg pageMsg


type PageMsg previous current

Pages

A "page" is something you can select in the sidebar to display when the app is running. Pages can contain a single widget, tables showing every variation of your button components, or an entire login page. It's up to you!

firstPage : String -> Page model msg flags -> PageBuilder ( (), model ) (PageMsg () msg) flags

The first page in your UI explorer. This is the default page if the user doesn't specify a url path.

import Element
import UiExplorer

pages =
    UiExplorer.firstPage
        "My first page"
        (UiExplorer.static (\_ _ -> Element.text "Hi!"))

nextPage : String -> Page model msg flags -> PageBuilder modelPrevious msgPrevious flags -> PageBuilder ( modelPrevious, model ) (PageMsg msgPrevious msg) flags

Additional pages in your UI explorer. You have to start with firstPage before chaining the result to nextPages. Each page must also have a unique name.

import Element
import UiExplorer

pages =
    UiExplorer.firstPage
        "My first page"
        (UiExplorer.static (\_ _ -> Element.text "Hi!"))
        |> UiExplorer.nextPage
            "My second page"
            (UiExplorer.static (\_ _ -> Element.none))

groupPages : String -> (PageBuilder a0 b0 c0 -> PageBuilder a1 b1 c1) -> PageBuilder a0 b0 c0 -> PageBuilder a1 b1 c1

If your list of pages on the sidebar is starting to get too long, you can group some of them together with groupPages.

import MyPages
import UiExplorer

flatPages =
    UiExplorer.firstPage "Login" MyPage.login
        |> UiExplorer.nextPage "About us" MyPage.aboutUs
        |> UiExplorer.nextPage "Fonts" MyPage.fonts
        |> UiExplorer.nextPage "Colors" MyPage.colors
        |> UiExplorer.nextPage "Basic components" MyPage.basicComponents
        |> UiExplorer.nextPage "Homepage" MyPage.homepage

nowItsGrouped =
    UiExplorer.firstPage "Login" MyPage.login
        |> UiExplorer.nextPage "About us" MyPage.aboutUs
        |> UiExplorer.groupPages "Building blocks"
            (UiExplorer.nextPage "Fonts" MyPage.fonts
                >> UiExplorer.nextPage "Colors" MyPage.colors
                >> UiExplorer.nextPage "Basic components" MyPage.basicComponents
            )
        |> UiExplorer.nextPage "Homepage" MyPage.homepage

Two things to note:

static : (PageSize -> flags -> Element msg) -> Page flags msg flags

A page that doesn't change or react to user input. It's just a view function.

getPagePaths : List String -> PageBuilder model msg flags -> List String

Get the relative paths to all the pages you've defined.

pages =
    UiExplorer.firstPage "First page" (UiExplorer.static (\_ _ -> Element.none))
        |> UiExplorer.nextPage "Second page" (UiExplorer.static (\_ _ -> Element.none))

paths =
    -- [ "/First%20page", "/Second%20page" ]
    getPagePaths [] pages

pathWithSubdomain =
    -- [ "/subdomain/First%20page", "/subdomain/Second%20page" ]
    getPagePaths [ "subdomain" ] pages


type alias Page model msg flags =
{ init : flags -> ( model
, Platform.Cmd.Cmd msg )
, update : msg -> model -> ( model
, Platform.Cmd.Cmd msg )
, view : PageSize -> model -> Element msg
, subscriptions : model -> Platform.Sub.Sub msg 
}

All the functions you need for wiring together an interactive page. It's basically just Browser.element.

import MyCoolUi
import UiExplorer

loginPage =
    { init = MyCoolUi.loginInit
    , update = MyCoolUi.loginUpdate
    , view = \pageSize model -> MyCoolUi.loginView model
    , subscriptions = always Sub.none
    }

pages =
    UiExplorer.firstPage "Login Form" loginPage


type alias PageSize =
{ width : Quantity Basics.Int Pixels
, height : Quantity Basics.Int Pixels 
}

The size of the page your UI gets placed in. This is not the same as Browser.Events.resize since the UI explorer displays a sidebar that can take up some of the window space.

You'll need ianmackenzie/elm-units in order to use Quantity Int Pixels.

import Pixels

getWidth : PageSize -> Int
getWidth pageSize =
    Pixels.inPixels pageSize.width


type PageBuilder model msg flags