Here's a non-trivial container component: TabbedPages

321 views
Skip to first unread message

Hassan Hayat

unread,
Jul 20, 2015, 11:28:17 PM7/20/15
to elm-d...@googlegroups.com
Hey there,

So, I've tried to make a non-trivial container component to try to apply all the lessons I've been learning in working with Elm and the Elm Architecture. You can find it here: https://github.com/TheSeamau5/TabbedPages and you can try locally the example under src/Main.elm

The idea behind this component is that it is a container component that has two container components. (super meta i know) It's the good old, tabs + pages combo. 

1) There are tabs at the top, and when you select a tab a different page appears. 
2) You can swipe the pages left and right to go to the next page and previous pages.
3) The pages animate with physics-based animations using my work-in-progress library elm-spring
4) The tabs have a little strip that sits under the currently selected tab which also moves using physics-based animations 
5) The tab strip also moves in lockstep with the swiping of the pages.
6) Both the tabs and the pages are completely generic and do not care what their children are.
7) Both the tabs and the pages provide contexts in order to feed data to the children. In the example, I use this to change the color of the selected tab item.


Unfortunately, it would be a bit too much work to write a tutorial on this, but allow me to just show the main types. First of all, I use the following type aliases


type alias Init options stateVector -> options -> state 
type alias Update action stateaction -> state -> state 
type alias View action stateAddress action -> state -> Html


The "Init" is the type of the initializers. Initializers take a size, an options object and return a state.

The "Update" function is just the simple reducer that updates the state given an action.

The "View" function is just the function to view a state given an address.



Here are the types: 

Label.elm (A simple component)

init : Init Options State

update : Update Action State

view : View Action State



Tabs.elm (A container component of unknown number of unknown children that communicates a context to its children)

init : Update Context itemState
    -> Init itemOptions itemState
    -> Init (Options itemOptions) (State itemState)

update : Update Context itemState
      -> Update itemAction itemState
      -> Update (Action itemAction) (State itemState)

view : View itemAction itemState
    -> View (Action itemAction) (State itemState)



SwipePages.elm (A container component of unknown number of unknown children that communicates a context to its children)
init : Update Context pageState
    -> Init pageOptions pageState
    -> Init (Options pageOptions) (State pageState)

update : Update Context pageState
      -> Update pageAction pageState
      -> Update (Action pageAction) (State pageState)

view : View pageAction pageState
    -> View (Action pageAction) (State pageState)


TabbedPages.elm (A container component with a tabs container and a pages container, both synchronized, both have unknown number of unknown children and both communicate with their children)

init : Update Tabs.Context tabState
    -> Init tabOptions tabState
    -> Update Pages.Context pageState
    -> Init pageOptions pageState
    -> Init (Options tabOptions pageOptions) (State tabState pageState)

update : Update Tabs.Context tabState
      -> Update tabAction tabState
      -> Update Pages.Context pageState
      -> Update pageAction pageState
      -> Update (Action tabAction pageAction) (State tabState pageState)

view : View tabAction tabState
    -> View pageAction pageState
    -> View (Action tabAction pageAction) (State tabState pageState)


Some quick lessons I've learned: 

1) The most practical way to apply a context is via a simple reducer. "Update Context state". You can still use function composition and it is much easier to work with. (no need to produce a single action or to wonder what to do when no actions can be produce)

Here are the ones I use in the code for illustration

applyPagesContext : Update Pages.Context Label.State
applyPagesContext context state =
  state
  |> Label.update (Label.Resize context.size)

applyTabsContext : Update Tabs.Context Label.State
applyTabsContext context state =
  if context.isSelected
  then
    state
    |> Label.update (Label.SetColor Color.red)
    |> Label.update (Label.Resize context.size)
  else
    state
    |> Label.update (Label.SetColor Color.blue)
    |> Label.update (Label.Resize context.size)


2) Initializers for container components that communicate contexts require the context reducer. Concretely, if this weren't the case, at the start of the application, none of the tabs would "look" selected (i.e. all the labels would be blue). You need to apply the context then in order to see the red color from the very beginning. 

3) Container components that try to synchronize two components will need to intercept almost all of the child actions. Just go under TabbedPages.elm and you'll see the update function. It looks long but it's actually super simple. The gist of it is: when the tabs receive an action to select the third tab, tabbed pages intercepts this to make sure the pages also select the third pages.

4) It is important to be very explicit with actions such as Resize. If you do any animations, this action could potentially break everything. Make sure to handle it with care such that everything gets repositioned accurately. 



Finally, I'd like to add that the code is far from perfect (and it is not devoid of bugs). I also wanted to add touch support but I couldn't figure out how. 

In any case, I think it illustrates how one could make such generic components that also have this synchronization going on. Also, I'd like to invite anyone interested to try their hand at improving the code. There is a lot of redundant and very similar code. And, there's a lot to gain in finding ways to get code like this to be simpler and easier to use. For example, one question I can think of: Is the fact that the update part of TabbedView is so explicit and redundant a good or a bad thing. Note that a lot of frameworks handle this with two-way data-binding.  


If there are any questions about the code or the ideas, I'd be glad to answer,
Hassan Hayat

Evan Czaplicki

unread,
Jul 21, 2015, 12:10:04 AM7/21/15
to elm-d...@googlegroups.com
Thank you for sharing this! The result looks excellent! I kind of want to not look at the code for now and keep going with my exploration. Maybe when I get further we can compare where we got to independently and figure out what seems to work best.

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Hassan Hayat

unread,
Jul 21, 2015, 3:50:16 AM7/21/15
to elm-d...@googlegroups.com
Thank you for sharing this! The result looks excellent! 
 
Thanks.

 I kind of want to not look at the code for now and keep going with my exploration. Maybe when I get further we can compare where we got to independently and figure out what seems to work best.
 
Sure, as you wish. I just have to point out that my exploration does not deal with tasks and uses exclusively inline styles (which is not the case with your exploration). But, sure, I'd be super glad to compare our findings whenever you feel ready. 

Hassan Hayat

unread,
Jul 21, 2015, 3:54:01 AM7/21/15
to elm-d...@googlegroups.com
Oh, and a last point. It took me an afternoon + an evening to do it (knowing that I didn't exclusively do this yesterday). I honestly thought it would take me way longer to do. So, 40 points to Gryffindor... I mean Elm for productivity :D
Reply all
Reply to author
Forward
0 new messages