Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

communication between components in Elm

Tags:

elm

Let's say I am trying to follow the Elm architecture and split my workflow into Users and Invoices while using StartApp.

Users have invoices, but they must log in to access them.

The model may look like something along these lines:

type Model
   = NotLoggedIn Credentials
   | LoggedIn RealName (Maybe Invoices)

type alias State =
   { login : Model
   , notification : ......
   , ......


type alias Invoices = { invoices: List Invoice, ...... }

User module has actions:

type Action
   = Login (Result Http.Error String)
   | Logout
   | Submit
   ...

and update function:

update : Action -> Model -> (Model, Effects Action, Notification)
update action user =
   case (action, user) of
      (Login res, _) ->
         case res of
            Ok name ->
               (LoggedIn name Nothing, Effects.none, Info "Welcome!")
   ...

I skip the details of authentication, it's all good. The interesting part is Login action. The tuple is sent to the step function in main:

step : Action -> State -> (State, Effects Action)
step action state =
   case action of
      UserAction a ->
         let (newstate, ef, n) = User.update a state.login
         in ({ state | login = newstate, notification = n }, Effects.map UserAction ef)
      InvoiceAction a -> ......

So the user has logged in. Next we want to call some init action in the Invoice module.

But how should this be done properly? How to initiate the action of Invoice to preserve incapsulation? Shall I return sometihng other than Effects.none?

like image 825
punund Avatar asked Mar 30 '16 18:03

punund


2 Answers

This might be a case that could be solved by different modeling of your app's data.

The way I understand it, you have actions that require as user and actions that do not require an user. The InvoiceAction seams to me that it should belong to the UserAction.

So, you could have

type MainAction = UserAction UAction | NonUserAction NonUAction 

type UAction = AuthAction Credentials | InvoiceAction Invoice.Action

The user model would encapsulate both login details and invoice details. And then, after a successful log in you could redirect to an InvoiceAction.

update action model =
  case action of 
    AuthAction credentials -> 
      let 
        (isLoggedIn, notifications) = Authentication.check credentials
        model' = { model | credentials = credentials, notifications = notifications}
      in 
        if isLoggedIn
        then update (Invoice.initialize model'.credentials) model'
        else (model', Effects.none)

    InvoiceAction act -> 
      let 
        (invoices, fx) = Invoice.update model.credentials act model.invoices
      in 
        ({model | invoices = invoices}, Effects.map InvoiceAction fx)

The actual action is provided by the Invoice module via a function initialize with a signature like initialize: Credentials -> Action. This is done to maintain encapsulation. The User module need not know about particular Invoice actions, only that there is one related to initialization and it can get it via that function.

Also, please note that I've simplified the update signature and moved the notifications inside the model. This is a personal preference as I see notifications as nothing special. They are like any other piece of data in the model. Of course, if the notifications are tasks that get routed via some custom StartApp into a port and get displayed by some JS mechanism, it might make sense to keep them in the return.

like image 55
pdamoc Avatar answered Oct 13 '22 20:10

pdamoc


One approach will be:

  • Create a mailbox in the parent component
  • Pass the address of that mailbox to User update
  • User update returns an effect that sends a message to this address
  • Upon receiving a message this mailbox triggers an action that flows to Invoice

This chapter in elm-tutorial shows this pattern.

like image 23
Sebastian Avatar answered Oct 13 '22 20:10

Sebastian