Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formless 3 #82

Merged
merged 3 commits into from
Dec 27, 2021
Merged

Formless 3 #82

merged 3 commits into from
Dec 27, 2021

Conversation

thomashoneyman
Copy link
Owner

@thomashoneyman thomashoneyman commented Dec 27, 2021

What does this pull request do?

Totally rewrites Formless into a single file!

Along the way, I've made several significant usability improvements. The highlights include:

  • No more specs, newtypes, or eval action = handleAction handleQuery handleEvent action awkwardness (no more wrapInputFields or unwrapOutputFields, either!)
  • No more Initial type class: form fields can have any input, error, or output types you want without restriction
  • Form state can be accessed directly, with no proxies or lenses (no more mkSProxies!)
  • Validation can be run in HalogenM with full access to read and modify the form state (no more specialized Validation module!) and can be run on change, on blur, or whenever you want!
  • Variants no longer creep into user code (apart from validation), so you don't need to use injAction or injQuery -- just write your actions and queries as you would in any other Halogen component

In sum: forms are easier to write with fewer weird restrictions and much more power. To check it out, please see the examples site and source code.

What are the next steps?

I can use your help! I need people to:

  • Test the documentation site out and make sure the forms work as expected
  • Ask me for specific examples that don't overlap with the existing ones, which demonstrate things you want to do in your forms (for instance: I intend to implement a 'form wizard' example, and an example with custom child components).
  • Try out the new Formless on new and existing forms and see what feels ergonomic and what feels clunky. There's a lot of room to add documentation for Formless, but I need help seeing what concepts and issues folks are stumbling on.

To talk about any of this, please feel free to open issues here, or chat with me on the PureScript chat in the #halogen channel, or open discussions on the PureScript forum

When will Formless 3 be released?

Formless 3 relies on a pull request by @MonoidMusician that implements variant mapping for the variant library. Until that pull request is merged, this will remain unreleased and only available via pre-release tags. (Don't worry -- despite being opened in 2018, we've been talking about moving it forward recently!)

I would like to use the period from now until release to make other breaking changes and implement feature requests before things get set in stone for Formless 3.

Summary of Changes

Formless has been in an awkward state over the past year or two. After writing halogen-hooks I wanted to rewrite it in Hooks, but the ensuing types didn't feel ergonomic and I dropped the project (you can still see the branch, though!). After some time away I realized there was a way to get rid of all the newtypes and complicated type classes and lenses and proxies and significantly simplify the implementation and use of Formless.

Here are the primary changes:

Move to a higher-order component

I first wrote Formless against Halogen 4. Since then, Halogen has become much nicer for writing higher-order components that transparently pass through queries, inputs, and outputs. Accordingly, the new version of Formless (which works with Halogen 6) is a higher-order component. The higher-order component accepts:

  • a FormConfig that only requires one field (a way to lift Formless actions into your component action type), but optionally can specify several values to control the behavior of Formless
  • a set of input values, which no longer have any restrictions on their types (ie. you don't need an Initial instance); most simple forms can just provide mempty for this
  • your form component

The last bit is the major difference between the old and new Formless.

The old approach was to provide you with a component that already had a form state and an extensible variant of actions for interacting with it. It also already had a handleAction for the existing actions, but you could inject your own actions into the variant and then handle them. The component took a Spec that let you provide your own handleAction, handleQuery, and so on. You could be notified about events in the form (like submission) by providing a handleEvent function to take care of it. The end result worked, but it was clunky.

The new approach inverts a few things. Now, you write a component from scratch that is required to accept the form state and actions for interacting with it as component input. When you want to call an action, you raise it to Formless for execution. When an event happens in the form, you're notified via a query. Since your component and Formless are entirely separate except for the input/query/output interface, you have a lot more control over your component and it feels much more ergonomic to write.

Transfer validation and events to queries

One of the most significant changes in Formless 3 is an inversion of who is responsible for validating fields and handling events.

The old approach was to accept a handleEvent function for handling events, and validation functions for each field in the form. When an event arose, or a field was ready to be validated, Formless would call these handlers and then do something with the result. You wrote the handler, but then you handed it off.

This works, but it's less powerful than it could be. The new approach is to move events and validation into queries, so that Formless can query your component when an event occurs or validation needs to happen, and then you can handle the event or run validation in your component's HalogenM and decide how or if to reply. The mental model is easier: this is normal Halogen parent-child communication. And validation is strictly more powerful: you can now validate in HalogenM, which means you can access the form state, trigger actions (like setting the values of dependent fields), and more.

The new model is powerful enough to match the old Formless functionality without requiring any queries, inputs, or outputs for the new Formless component. In other words, you can write a form component and only accept queries and input relevant to your specific use case. Or you can replicate the old Formless interface, if you wanted!

Remove newtypes

The final major change to Formless regards the form type itself. Formless has always been designed to minimize the number of Form-related types you have to define over and over again, and to be able to transform that input type into several others. But the downside has always been that you can't just describe those transformations with type synonyms (because it would lead to the type synonyms being partially-applied).

The old component focused on your form row, passing it various arguments. For example, you could define a form row and then define a type for passing in a record of just inputs like this:

newtype MyForm r f = MyForm r ( name :: f Input Error Output )

-- in Formless
newtype InputField input error output = InputField input

component :: Newtype (form Record InputField) { | inputs } => form Record InputField -> ...

This was nice because every derivative type is based off of form alone. But the less-nice part was that doing this meant that every derivative type had to be a newtype, since type synonyms can't be partially-applied.

But all of these problems go away if you move the types into user-land, so Formless doesn't have any idea you've used a type synonym on your form row to produce a new type -- it just takes an input record of any fields and makes sure they work out:

type MyForm f = { name :: f Input Error Output }

-- in Formless
type FieldInput input error output = input

component :: MkFields inputs fields => { | fields } -> ...

Formless 3 uses this idea to invert who controls the types the same way it inverts who controls validation. The downside is that you now have to write more types than you used to, but the upside is that you never have to use a newtype, wrapInputFields, mkSProxies, field proxies, or lenses again. Essentially, Formless relies on you for type information and then checks that all your provided types work out together, instead of accepting just the form type and working all the types out for you. I think the tradeoff is absolutely worth it.

@thomashoneyman thomashoneyman merged commit 60546a7 into main Dec 27, 2021
@thomashoneyman thomashoneyman deleted the formless-3.0.0 branch December 27, 2021 04:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant