Hello! I’m new to Elm and am trying to translate to Elm this case:
<div class='card'>
<div class='title'>TITLE</div>
<div class='hider'>
<div class='content'>
Some Content Here
</div>
</div>
</div>
<style>
.hider {overflow:hidden;}
</style>
In Js I set a value to note if the card is open or closed. I also have a function that updates the view by setting <div class='hider'>.style.height to "0px" or to .clientHeight+'px' of <div class='content'> depending on the noted value. The update function is triggered by clicking on the <div class='title'> element.
For some days I struggled to implement it in Elm, but I can not find a way to do it. I tried to use Task.perform and Browser.Dom.getViewportOf to get clientHeight of the .content element and then get .viewport>>.height from it, and then put it in model and then use it in view to update .hider element [style "height" (if open then height else "0px")]. And then if I have variable number of this collapsible cards I somehow need to track open status for all of them. Elm does not have local state for separate blocks/parts/components like Vue/React. It’s getting more and more confusing.
So my concern is am I doing it right or am I heading a wrong direction? What would be ‘idiomatic’ way to do it in Elm?
Here is one way.
In your Elm Model you would have an attribute like
collapsedCardIDs: Set String
String here is the id for a card.
Then the title element will call a message
type Msg = ToggleCard String Bool
div [ onClick (Toggle cardID newState) ] [ text "TITLE" ]
cardID is a unique id for that card. newState is a boolean with the collapsed value for that card.
Then in update you put that cardID in the collapsedCardIDs set if True, remove it if False
ToggleCard cardID state ->
let
collapsedCardIDs =
if state then
Set.insert cardID model.collapsedCardIDs
else
Set.remove cardID model.collapsedCardIDs
in
( {model | collapsedCardIDs = collapsedCardIDs } , ...)
Finally in your view, you show the expanded part if the cardID is not in the collapsedCardIDs set.
let
expandedElement =
if Set.member model.collapsedCardIDs cardID then
text ""
else
div [] [ content... ]
in
div [] [
titleElement
, expandedElement
]
If you don’t need to control the cards programmatically (only based on events), but still want to animate the opening/closing, you can get the height of the card from the event directly, by using a custom event handler instead of the built-in Html.Events.onClick one. Every time an event is fired, you basically get a readonly view on the current DOM state through json decoders on the event itself, So it lets you just read the scrollHeight (or any other property) at that point. This means you don’t need to generate unique Ids/use commands to get the element’s height:
Yes! You’ve got the idea behind the .hider and .content parts! Sliding animation! I tried to apply your example to my playground site, and it works exactly as I wanted! And I see that I have to study much more - I don’t understand how it works, it’s much more complicated than its js version =)
Is it possible to set .hider height to the height of .content at first render? To have cards open at first load. I added type CardState = ... | NotSet, which sets .hider height to "", but then there is no closing animation at first time I click title.
Thank you for your reply, I’ve learned something new from it like how I can use Set to track data and general approach. My question was maybe a bit misleading, I am trying to make an animated widget that would slide close and open =) the hard part for me is to how get, store, update, and set content height for each card when I don’t know cards count and their content height in advance.