[RESOLVED] How to connect and deriveState for a Halogen Store from Formless component?

https://pursuit.purescript.org/packages/purescript-halogen-formless/2.0.1/docs/Formless

I’m not really sure which comes first when trying to make an F.component and Store.connect. Does deriveState now become the first argument to F.component to handle the input? If so, what on Earth are my types supposed to look like? If you build your State from Context+Input and then pass it to F.component how would that even work?

-- Does this even make sense?
-- I couldn't figure out what I needed in these type holes
deriveState ∷ 
   ∀ m. 
   Store.Connected Context (F.Input Form ?foo m) →
   F.State Form ?bar m
deriveState { context, input } = ...

component ∷
   ∀ m.
   MonadAff m ⇒
   Store.MonadStore Store.Action Store.Store m ⇒
   F.Component Form Query () Input Output m
component =
   Store.connect selectState
      $ F.component deriveState
      $ F.defaultSpec
         { handleAction = handleAction
         , handleEvent = handleEvent
         , handleQuery = handleQuery
         , initialize = Just InitializeForm
         , render = render
         }
   where
   ...

With an error that seems cryptic

  Could not match type
  
    () @Type
  
  with type
  
    ( validators :: Form Record (Validation t4 t5)
    | t3
    )
1 Like

@thomashoneyman is this another hoist situation?

I don’t know the answer to your question directly.

But I wonder — instead of the ConnectedInput / context approach — if you’d have an easier time of it by subscribe-ing to the result of Halogen.Store.Monad.emitSelected in your form component’s InitializeForm action handler.

That is an interesting approach – the biggest downside is that you need empty values to accept the input, and you can’t “select” what you are subscribing to. The Store library is quite new so the examples are quite minimal. And trying to follow the rabbit hole of Row Type errors from Formless has proven difficult to follow the compiler errors to me. A lot of this is on me though.

This ended up working for me using Halogen.Store.Monad.getStore + Halogen.Store.Connect.subscribe on init. I put in dummy/mempty values for the formInput init function.

1 Like

Sorry I haven’t responded — I am on vacation until Monday, but I can get back to this then!

2 Likes

I put together an example on Try PureScript that demonstrates using Store + Formless together:

https://try.purescript.org/?gist=42f82d519b78e7a0664d42696bfd87ea

It’s…more convoluted than I would like, but in this example the initial state of the “name” form input is set from the global state, and when submitting the form the name that you submitted gets set as the global state. Hopefully this is a decent starting point for you to take this where you need to.

Working on this example has emphasized how clunky this library is :grimacing: I do hope to come back and clean it up soon.

3 Likes

Ah, so you did make the context a part of the parent’s state so you could pass it in as an input to the child Formless component. I was looking for a way to avoid needing to subscribe to the Store at the parent if not needed. Yes, you are using it to show the Global Name, but there are cases where that might not be the case. I thought this was the one of the bigger appeals to Store is not having to hold onto and pass in state.

Concretely, I have created a global emitter for the browser’s connectivity state, and many of my forms are subscribing to this to disable the form submission and show a message for the browser being Connectivity.Offline My other components up the chain do not care about offline.

2 Likes

I see. You can also have the form component subscribe to the store directly, and then keep that state as part of the form only. I’ve adjusted the example here to demonstrate that:

https://try.purescript.org/?gist=3b1b9c7cd5e7b8493b1a3b42aa04b7ea

If it’s easier, you can see the gist directly here:

2 Likes

I think where I got tripped up is not selectAll but selectEq where my types were an absolute mess between input and the selector needing all of the Formless bits. :\ With a little more complex of state and context, I got lost.

I think once you start adding more parts, it gets more confusing. Here’s an adjusted source via gist Try PureScript!

1 Like

This code is problematic:

-- What is this type? Is this even sensible?
deriveState
  :: forall m
   . Monad m
  => Connected Store (F.Input DogForm FormState m)
  -> F.State DogForm FormState m
deriveState { context, input } = 
  { connection: context.connection
  -- wat
  --, dirty: input.dirty
  --, validators: input.validators  
  }

There are a couple things going on here.

First, keep in mind that Formless is not a usual component and it manages a lot of state on your behalf. If you find yourself constructing the full Formless state then it’s a sign something is wrong. Instead, you generally let Formless manage its own state and you just add extra fields to it and modify those fields you added.

Second, your deriveState function is trying to use the Formless.Input type to construct a full Formless state, but the state contains many fields that the input type does not. For example, there is no { dirty :: Boolean } field on the Formless.Input type.

Third, your form component’s receiver will only receive the input that you specify in your types and that is sent directly from the parent component or store. It doesn’t receive the full Formless.Input type. In your deriveState function you are using the type (Connected Store (Formless.Input form state input)), but your form component will not actually receive that as its input. Instead, it will receive only (Connected Store input).

Arguably this should change in Formless, because you already do provide a function input -> Formless.Input form state input that could be used:

src/Formless/Component.purs#L118

…and this mkInput function is used when constructing the initial state:

src/Formless/Component.purs#L122

…but it isn’t used when receiving:

src/Formless/Component.purs#L128

…even though we can and probably should use it to produce a new Formless.Input type. Regardless, even if we did do that, it wouldn’t do anything to help the situation here.


With all of this in mind, I would recommend not writing a deriveState function that creates a brand-new state. Instead, you want to modify the existing state. Namely, you could write something like:

deriveState 
  :: forall m
   . Monad m 
  => Connected Context { isFoo :: Boolean }
  -> (F.State DogForm FormState m -> F.State DogForm FormState m)
deriveState { context, input } = _
  { connection = context.connection
  , isFoo = input.isFoo
  }

and use it in your receiver:

FormReceive connectedInput ->
  H.modify_ $ deriveState connectedInput

You can use your connected input to create your initial Formless.Input, of course:

mkInput { context, input } =
  { connection: context.connection
  , isFoo: input.isFoo
  , initialInputs: ...
  , validators: ...
  }

Here’s a modified version of your code, with some input from the parent and some input from the store:
https://try.purescript.org/?gist=2efb8bd56e13f19c12e1858b5f66ce69

2 Likes

Ha! Thanks for going through this to pick out of me what I was really trying to ask. The fact I was reconstructing the state was a massive red flag, which is why I came here for help. This all makes a lot more sense now.

2 Likes