Call for feedback: Formless, a new form library for Halogen

halogen
renderless

#1

Over the past few weeks I’ve been building Formless, a new form library for Halogen apps.

Formless lets you write complex forms as a Halogen component in a few dozen lines of code, managing form state, validation, errors, parsing, resetting, submission, and so on for you with no opinion at all on your rendering. You can freely include external components, layer new functionality on top, or control the component via what queries you include in your HTML.

I released a usable v0.1.0 on Friday along with a live examples / documentation site. The source code for the examples is also available, and the README contains a short overview.

Formless v0.1.0 is already better-enough than the previous way my team did forms at CitizenNet that we’re putting it in production across most of the app, and have already replaced a complex 1,500-sloc form. But Formless is still far away from being as good as it could be or covering as many use cases as it ought to.

I could use some help and advice from the community to make Formless better. If you’re deeply knowledgeable about PureScript or Halogen, I have some questions below that could really use your help! If you’re still a beginner or you’re interested in trying a new approach for forms in your app, I would love to work with you to explain the concepts and put together better documentation and a tutorial.

I have added 4 responses to this post, each of which is an area of improvement for the library. If you notice a place where you could offer some feedback, I would love to hear from you in a reply!

formless-realworld


#2

Use Cases

Formless has been used for small, 2-3 field forms all the way up to forms spanning multiple pages and dozens of fields including various input types like sliders, radio buttons, typeahead components, and so on. But there are a few use cases that I haven’t worked on, including:

  • Nested forms and data types
  • Validation between forms, where a value on page 3 has to match a value on page 2

If you’re working on forms in your application, have a particular use case you’re not sure Formless supports, and there is no relevant example in the live examples site, then please respond here and let me know! I’ll build an example that matches your use case, or work with you to build it if you’re interested in contributing.


#3

Accessing a particular field

Because you write your form on your own and pass it in to Formless, the component doesn’t have any idea what’s in it. All it knows about is the vague shape of the data.

What I want in particular is the ability to access one particular field in the form in the component. For example, I’d like the user to be able to provide a variant (maybe possible?) or a symbol proxy (not possible) or something that provides access to a single field that I can then use to modify that field only.

For the time being, I supply helper functions that, given a symbol proxy for a field in the form, construct a lens getter or setter that can be applied to the entire thing, and then Formless runs this on the entire form instead of on a particular field. I would much rather that a user could provide an identifier for a particular field and then I can run validation or updates or whatever on just that field.

Why is this a problem? The main reason is that I can’t perform actions on a particular field when only that field changes. For example, say you have an expensive validation to run on a field (some server call) and you only want to run it when that field is blurred. But Formless only lets you provide whole-form transformations, so every time validation for the form as a whole is triggered, your expensive computation will run.

The long version

Why can’t I do this now?

Formless expects you to provide a “Form” type and an accompanying form spec. The form type should be a newtype over a record containing each of the fields in the form, with each field’s error, input, and output type specified. The form spec should be the same set of fields with their initial input values set.

For example:

import Formless as F

newtype Form f = Form { name :: f Err String Name, text :: f Void String String }
derive instance newtypeForm :: Newtype (Form f) _

myFormSpec :: Form F.FormSpec
myFormSpec = Form { name: F.FormSpec "", text: F.FormSpec "" }

-- It can also be generated for you to avoid having to write all the newtypes
myFormSpec' = F.mkFormSpec { name: "", text: "" }
myFormSpec'' = F.mkFormSpecFromRow $ RProxy :: RPoxy (name :: String, text :: String)

Formless has a few types that it can fill in for f above, like:

newtype InputField e i o = InputField
  { input :: i
  , touched :: Boolean
  , result :: Maybe (V e o)
  } 

So it at least knows it can pull out an input, run validation on it, and store the result as well as maintain touched states and so on. Not knowing the contents of the fields is limiting in the query algebra, however, because I pretty much just have access to whatever I can plug in for f. The two most common queries called are these:

data Query ...
  = Modify (form InputField -> form InputField) a
  | ModifyValidate (form InputField -> form InputField) a

in which I expect the user to give me a function with which to modify the entire form at once. Validation, similarly, is applied to the entire form at once, and it’s not possible for me to laser in on any particular field. So instead I provide helper functions that, given a symbol proxy for a particular field in your form, will create a lens setter or getter that you can give to Formless to create this form -> form function.


#4

Validation

In Formless, I rely on a function form InputField -> m (form InputField) that you provide for validation. You can make this function by providing field-level validators from whatever library you want (purescript-validation and purescript-polyform have helper functions available). It looks like this…

newtype Form f = Form { name :: f Err String Name, text :: f Unit String String }

validator :: ∀ m. Monad m => Form Formless.InputField -> m (Form Formless.InputField)
validator = Formless.applyOnInputFields
  { name: Name <$> MyValidation.minLength 5
  , text: MyValidation.notRequired
  }

-- With types:
validator :: ∀ m. Monad m => Form Formless.InputField -> m (Form Formless.InputField)
validator = Formless.applyOnInputFields
  { name: (Name <$> MyValidation.minLength 5) :: Polyform.Validation m Err String Name
  , text: (MyValidation.notRequired :: Polyform.Validation m Unit String String)
  }

This will, on any trigger of the Validation query in Formless, run this function on all your form’s input values, excluding those which haven’t been touched by the user yet.

Applicative-style validation doesn’t always work when it comes to forms for a few reasons: you need to be able to view the entire result (errors and success) for rendering purposes; for user experience it’s common to not validate fields unless they’ve been touched; for many fields you’d like validation to depend on earlier validations (like validate a string as an integer, then validate the integer is under some limit); for some fields your validation needs to be effectful, like checking that an email address is not already in use via your server.

This function is my attempt to provide usable validation of the sort I’ve described above while relying on existing libraries in the ecosystem. But it’s a little unprincipled, and because of the “Accessing a particular field” problem in the previous post, validation always has to run on the entire form and can not be triggered on just one field in particular.

I’m open to other approaches to validation.


#5

SProxy boilerplate

Formless relies heavily on record manipulation, particularly getting and setting. For that reason, when you write out a Form newtype it’s also common to write out proxies for every field:

newtype Form f = Form { name :: f Err String Name }
derive instance newtypeForm :: Newtype (Form f) _

_name = SProxy :: SProxy "name"

-- In use...
render = ...
   [ HE.onValueInput $ HE.input $ Formless.ModifyValidate <<< Formless.setInput _name ]

However, in a large form, it’s pretty tedious to write out every single field again as a symbol proxy, and with lots of forms in your application there will be plenty of duplicated names hanging out in various modules. There is some exploratory work in progress on generating a record of lenses for your form, with help from @monoidmusician, but it’s unclear whether that would be a better approach.

I’m open to hearing if there are better ways to approach making it easy to get / set fields in the form without tons of boilerplate.


#6

I’ve ultimately settled on generating a record of proxies from a form, which can then be used to construct lenses throughout the form’s use. The class and use look like this, and are more useful for medium-to-large forms than small ones:

newtype Form f = Form
  { name :: f Void String String
  , email :: f Void String String
  , city :: f Void Int String
  , other :: f Int String Int
  }
derive instance newtypeForm :: Newtype (Form f) _

-- This is a record filled with SProxies
proxies :: Proxies Form
proxies = mkSProxies (FormProxy :: FormProxy Form)

The underlying implementation:

type Proxies (form :: (Type -> Type -> Type -> Type) -> Type) =
  forall row xs row'
    . RL.RowToList row xs
   => MakeSProxies xs row'
   => Newtype (form FormSpec) (Record row)
   => Record row'

mkSProxies
  :: ∀ form row xs row'
   . RL.RowToList row xs
  => MakeSProxies xs row'
  => Newtype (form FormSpec) (Record row)
  => FormProxy form
  -> Record row'
mkSProxies _ = Internal.fromScratch builder
  where
    builder = makeSProxiesBuilder (RLProxy :: RLProxy xs)

class MakeSProxies (xs :: RL.RowList) (to :: # Type) | xs -> to where
  makeSProxiesBuilder :: RLProxy xs -> Internal.FromScratch to

instance makeSProxiesNil :: MakeSProxies RL.Nil () where
  makeSProxiesBuilder _ = identity

instance makeSProxiesCons
  :: ( IsSymbol name
     , Internal.Row1Cons name (SProxy name) from to
     , MakeSProxies tail from
     )
  => MakeSProxies (RL.Cons name x tail) to where
  makeSProxiesBuilder _ = first <<< rest
    where
      rest = makeSProxiesBuilder (RLProxy :: RLProxy tail)
      first = Builder.insert (SProxy :: SProxy name) (SProxy :: SProxy name)

#7

It turns out this is solvable!

If you use the same row you used to create your form to also create a variant of the same fields, you can use that variant to send just one field at a time over to Formless. In turn, Formless can then use that variant to validate only one field at a time (or modify one, or whatever). So rather than a lens applied to the entire form, you supply a variant, and Formless will take care of selecting the proper field and updating it without running any other fields.

The monadic / side-effect validation problem is solved, without any lost functionality.

PR open here:


#8

With the proxy and per-field access problems handled, Formless will be ready for a 1.0 release in the next week or two.