thomashoneyman / purescript-halogen-formless

Forms for Halogen
https://thomashoneyman.github.io/purescript-halogen-formless
MIT License
138 stars 32 forks source link

Raised actions from external components are not reaching the parent #25

Closed dariooddenino closed 5 years ago

dariooddenino commented 5 years ago

I'm not sure if I'm doing something wrong, but I can't manage to thread messages from external components to the parent. I get no errors, but just silent fails so I'm having a hard time figuring out what's wrong. I'm using version 0.2.0

Edit to add that if I build the examples they work correctly. There has to be something that I'm failing to see here (but that should give me an error :( )

I've reduced my code to a pretty simple case:

data Query a 
  = HandleFormless (F.Message Query Form FormState) a
  | Typeahead TAMessage a

data TAQuery a = HandleSelect a

data TAMessage = SelectionsChanged

type ChildQuery = F.Query Query TAQuery Unit Form FormState Aff
type ChildSlot = Unit

component :: H.Component HH.HTML Query Input Void Aff
component =
  H.parentComponent
    { initialState: identity
    , render
    , eval
    , receiver: const Nothing
    }

   render :: State -> H.ParentHTML Query ChildQuery ChildSlot Aff
   render s = HH.div_ 
                    [ HH.slot unit F.Component { inputs, validators, submitter, render } 
                      (HE.input HandleFormless) 
                    ]

  eval :: Query ~> H.ParentDSL State Query ChildQuery ChildSlot Void Aff
  eval (Typeahead SelectionsChanged next) = next <$
    logShow "msg" -- <--- this is never called.

inputs ...
validators ...
submitter ...

render :: F.State Form FormState Aff -> F.HTML Query TAQuery Unit Form FormState Aff
render state = HH.div_ [ HH.slot unit tacomp unit 
                                    (HE.input $ F.Raise <<< H.action <<< Typeahead) 
                                  ]

tacomp :: H.Component HH.HTML TAQuery Unit TAMessage Aff
tacomp = 
  H.component
    { initialState: identity
    , render
    , eval
    , receiver: const Nothing
    }

    where
      render :: Unit -> H.ComponentHTML TAQuery
      render st = HH.button [ HE.onClick $ HE.input_ HandleSelect
                            , HP.type_ HP.ButtonButton
                            ] [ HH.text "CLICK" ]

      eval :: TAQuery ~> H.ComponentDSL Unit TAQuery TAMessage Aff
      eval (HandleSelect next) = next <$ do
        logShow "Check" -- <-- gets logged
        H.raise $ SelectionsChanged
thomashoneyman commented 5 years ago

Hi @dariooddenino! I'm sorry for the late reply; I've been on vacation in Japan for the past two days and will be here for another week before I'm back on a normal working schedule. I have an inkling of what might be wrong, but I haven't recreated your example. If my comments below don't help, let me know and I'll dig a little deeper tomorrow on the train.


The short version: I believe you need to handle the Formless outputs, and specifically the Emit message with the top-level component query inside.

The longer version (forgive me if this is not your issue!):

In your render function for the typeahead, you're triggering HandleSelect when the "CLICK" button is clicked. Next, your eval function for the typeahead kicks in, logging "Check" and then raising the message SelectionsChanged.

What happens next? Well, messages are the outputs of a component which get handled by their parent, and the parent in this case is Formless.

Formless, unfortunately, has no idea what to do with a SelectionsChanged message. But you know how to handle it, because you have a query written for exactly that: your top-level component's Typeahead query. And Formless does know what to do with top-level component queries: it has the Raise query and related Emit message. You can take your top-level component's query type, wrap it up in Raise (to make it a Formless query), and then Formless will send that query back to you via the Emit message whenever it gets triggered.

You've done that in your code: your handler for the typeahead component inside Formless is (HE.input $ F.Raise <<< H.action <<< Typeahead). You wrap up the message in Typeahead (your top-level component's query type), then wrap that up in Raise (a Formless query type). Finally, this handler gets called.

The eval function in Formless handles this new Raise message. It unwraps it to get the top-level component query inside, then sends an Emit message containing the query to your top-level component. So we repeat the pattern!

Now we're in your top-level component. It should handle the Emit message from the Formless component by unwrapping it and evaluating the query inside (after all, the query inside is a top-level component query):

eval (HandleFormless (F.Emit query) a) = eval query *> pure a)

So we've unwrapped the query, which is Typeahead message a, and called eval on it. Now, finally, your eval function in the top-level component will evaluate the Typeahead query and logged out the message and the process is complete.


To learn more about how renderless components work, see this commented file from purescript-halogen-renderless (I promise to write about the topic soon!):

https://github.com/thomashoneyman/purescript-halogen-renderless/blob/master/example/Child.purs

dariooddenino commented 5 years ago

Ugh, it's that for sure. I spent 3 hours analyzing carefully all the examples, and I missed it all the time :( I'm gonna kill myself.

Thanks!

thomashoneyman commented 5 years ago

Don't worry, it's not an obvious thing! Renderless components are a fairly new idea in Halogen and nothing has been written about them, though I gave a talk at LambdaConf this year that explains the concept. With only two examples of embedded parent queries and no real documentation it's an easy thing to miss. Rule of thumb: any time you use a renderless component like Select or Formless, go ahead and write your eval case for Emit first, handling the outputs recursively:

eval = case _ of
  HandleFormless message a -> case message of
    Emit query -> a <$ eval query

I'll add this to documentation soon :)

dariooddenino commented 5 years ago

Is there a vod of the talk by any chance? :)

thomashoneyman commented 5 years ago

While it was recorded, the LambdaConf 2018 videos haven't been released yet. Sorry about that! They should be coming out at any moment now.