This is the first port of a series:
Part II
Part III
I tried to reuse a small app and insert it in a larger app. The small app create a box with a name, a picture and a price that can be displayed or hidden clicking on the product box.
This is the small app that I used as starting point:
Before proceeding I added some style and some animation. This is the enhanced version:
Now I want to have the products to be in a list and I want to have the interface to display them all, each of them with its own independent interaction
So, first of all I rename the Model
to Product
Before:
type alias Model =
{ name : String
, price : Float
, priceInView : Bool
, picture : String
}
After:
type alias Product =
{ name : String
, price : Float
, priceInView : Bool
, picture : String
}
Then I create the new Model that is a list of products:
type alias Model =
{ productList : List Product
}
And I initialise it with a list of products:
init : Model
init =
Model
[ Product “Photocamera” 24.31 False “https://...jpg"
, Product “TV” 24.22 False “https://...jpg"
...
]
I need to identify each singular product now. I will do it using the position inside the List. Similar to what I have done before for the model, I rename the old view of the product to productView and I add a position parameter that is the position inside the List:
viewProduct : Product -> Int -> Html Msg
viewProduct product position =
div
[ if product.priceInView then
onClick (HidePrice position)
else
onClick (ShowPrice position)
...
Now the two messages sent when clicking have the position as parameter.
I updated the Msg
list as well with the Integer type:
type Msg
= ShowPrice Int
| HidePrice Int
So now the updated function will know which product has received the click.
I need now to create a view function that loop all the product and call the productView for each of them. To do so, I use List.indexedMap
:
This is the command
(List.indexedMap (\position product -> viewProduct product position) model.productList)
For each product in model.productList, this is calling viewProduct:
viewProduct product0 0
viewProduct product1 1
viewProduct product2 2
viewProduct product3 3
.. etc.
Now we need to change the update
function that is the most difficult part, probably.
First I need two helpers that extract and add back a product from the List based on its position.
The “getter” first convert the List into an Array with Array.fromList
, then it gets the product with Array.get
This return a Maybe Product. I use a condition to convert a Maybe product into a Product
This is the getter:
getProduct productList position =
let
product =
Array.get position (Array.fromList productList)
in
case product of
Nothing ->
Product ("PRODUCT NOT FOUND Position " ++ toString position ++ " not found") 0 False "" Just val ->
val
The “setter” use Array.set:
setProduct productList position product =
let
productArray = Array.set position product (Array.fromList productList)
in
Array.toList productArray
We are almost there.
Now using the setter and the getter I create a function that update the field princeInView using the parameter “value”. This function accept the list of products and return an updated new list of products.
updateProduct productList position value =
let
oldProduct = getProduct productList position
newProduct = { oldProduct | priceInView = value }
in
setProduct productList position newProduct
This function is needed in the new “update” function to update the model:
update msg model =
case msg of
ShowPrice position ->
{ model | productList = updateProduct model.productList position True } HidePrice position ->
{ model | productList = updateProduct model.productList position False }
This is all. You can find the final script here:
Is there a simpler way to implement this? Let me know in the comments below
Edited on April 30th, 2017
I tried to decoupled the product from the main file and move it in a separate module but I am having difficulties in using the Product.view from the main module because it returns this error:
Function `beginnerProgram` is expecting the argument to be:
{ …, view : Model -> Html Msg }
But it is:
{ …, view : { productList : ProductList } -> Html Product.Msg }
The problem seems to be the the system is expecting a Msg
but instead is getting a Product.Msg
because the view is coming from the module product.
Maybe using Html.map
could help to fix the problem, but I could not figure it out yet.
Edited 6 May 2017
See the Part II and III to see a solution to the problem above.
Also have a look at this code here, there is a different approach to display products on the page: