I received a question along the lines of the following:
Suppose I have two pages, one for a product list (received from the server) and another to show a listing for an individual product. How do I pass the product to the product page once it’s selected on the list page?
I remember that coming from JavaScript, I was also puzzled by similar scenarios, so let’s start with something like a JavaScript representation of the state, and refactor it into an Elm-like model.
In JavaScript, our model might look something like this:
{ page: "product" // could also be "list"
, products: [ { name: "Product 1", ... }, { name: "Product 2", ... }, ... ]
, selectedProductIndex: 5
}
Translated to Elm, the type of the model would be:
type alias Model =
{ page : String
, products : List Product
, selectedProductIndex : Int
}
This has a couple of issues:
page
is just a string, we could have an invalid value for the pageselectedProductIndex
could be out of bounds or out of date.However, Elm’s type system allows us to express these constraints in a way that eliminates invalid state.
First, let’s define a custom Page
type:
type Page
= ListPage
| ProductPage
type alias Model =
{ page : Page
, products : List Product
, selectedProductIndex : Int
}
Now we can only have a valid page in the model, but we can still have an invalid product index.
Logically, selectedProductIndex
doesn’t even make sense when we’re on the product list page. There is no selected product there!
The cool thing about custom types in Elm is that each tag can also be associated with a value. This means that we can move the index from the model record into the Page
type:
type Page
= ListPage
| ProductPage Int
type alias Model =
{ page : Page
, products : List Product
}
Now it only exists when the page value is ProductPage
!
That’s useful, but let’s consider the view code now:
view model =
case model.page of
ListPage ->
showProductList model.products
ProductPage index ->
showProduct index
showProductList products =
div [] <|
List.map (\product ->
div [ onClick <| UserSelectedProduct ??? ] [ ... ]
) products
We need to pass the index to showProduct
, and we need to produce UserSelectedProduct
messages in showProductList
. However, what’s readily available in showProductList
is the product
value rather than its index. In addition, this still leaves the possibility of having an invalid index.
So actually, the simplest thing to do is to pass the product value around, and get rid of the index altogether:
type Page
= ListPage
| ProductPage Product
type alias Model =
{ page : Page
, products : List Product
}
view model =
case model.page of
ListPage ->
showProductList model.products
ProductPage product ->
showProduct product
showProductList products =
div [] <|
List.map (\product ->
div [ onClick <| UserSelectedProduct product ] [ ... ]
) products
update msg model =
case msg of
...
UserSelectedProduct product ->
( { model | page = ProductPage product }, Cmd.none )
...
Note how model.page
is set in the update
function. With this setup, it’s no longer possible to have an invalid index!
This post illustrates a couple of key design principles for Elm code: