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

Integrate Polyform into Ocelot #36

Merged
merged 45 commits into from May 1, 2018

Conversation

Projects
None yet
2 participants
@thomashoneyman
Copy link
Member

thomashoneyman commented Apr 25, 2018

This PR suggests a few changes to the way we develop static forms. This work integrates Polyform for composable, well-typed, static forms.

Why?

  • Boilerplate: Building forms this way dramatically reduces all the duplication we had to put into the group form, from error maps and 'unvalidated/validated' forms to a new query for every field that needs to be updated and 'shouldValidate' maps in state and all that stuff. It still allows us all the power that we need to build that form, however, including the parsing.
  • Reusability: Composing forms from other fragments allows us to create reusable fragments in the first place. We can create a selection of fields and validations and then re-use them over and over again to create forms throughout the application.

Additions

There is a reasonable argument to be made that most of these modules should move into Wildcat altogether so that Ocelot remains focused on being a design system. We would create a Form module that contained our base functionality plus a collection of the various pre-built fields we'd like to compose into forms.

Modules

  1. src/Form/Form.purs
    This module is the base of our static forms. It defines how to turn a field into a form and how forms and validation can compose together to create greater forms. It defines helpers for turning a row of types into most of the other form types we need via higher-kinded data / type synonyms. It also provides a runForm function that, given a current state, some raw input, and our form transformation will produce a new form and maybe some valid, parsed, verified type as well.

  2. src/Form/Validation.purs
    This module collects our validators together. They are almost identical to our old validation, except they use Polyform's V instead of purescript-validation's type. These can be composed together.

  3. ui-guide/Utilities/Form.purs
    This module contains a few pre-made fields with their attributes and errors already set. We'd most likely want to create a collection of these in Wildcat to avoid specifying them every time. We can build forms out of these fields via formFromField and composition.

Examples

  1. test/Main.purs
    I have added a small test module to prove the approach. It can be deleted given there are now other examples, or it can be kept for small-scale testing without having to deal with rendering.

  2. ui-guide/Components/Validation.purs
    A fully-built form and Halogen component that showcases how the forms work. This is the place to see how this would work in practice in Wildcat.

Other Changes

  1. src/Blocks/FormField.purs
    There is no giant ValidationErrors sum type anymore. Instead, each field can have a variant of errors that need to be handled with a match case statement. This is nice because it means we can render much more fine-grained messages depending on where and when we are rendering a field, or we can always encode the error message into the variant and just unpack it. For that reason, I've changed this to just have an error field that might contain a string or might not.

Open Questions

Remaining Boilerplate

We still have places where we are writing boilerplate. Two major ones include:

  • Writing updateValue and updateValidate functions, which requires setting a symbol on every field in a record to a lens to itself. @crcornwell has experimented with generating these.
updateValue :: FieldValueV -> (State -> State)
updateValue = match
  { p1:    setValue _p1
  , p2:    setValue _p2
  , email: setValue _email
  }

Writing the initial form state. If we update to require a monad for fields, then we can use mempty everywhere and generate this one, too, but this isn't always possible (take something like a radio button).

{ email: { value: "", shouldValidate: false }
, password: { value: "", shouldValidate: false }
}

Tasks

  • Fix boilerplate around generating the initial record
  • Fix boilerplate around generating the updateValue and updateValidate functions
  • Remove attribute labels and fix boilerplate around generating the form spec from its types

thomashoneyman added some commits Apr 18, 2018

@thomashoneyman thomashoneyman requested review from davezuch and foresttoney Apr 25, 2018

@thomashoneyman thomashoneyman self-assigned this Apr 25, 2018

@thomashoneyman thomashoneyman requested a review from crcornwell Apr 25, 2018

@thomashoneyman

This comment has been minimized.

Copy link
Member

thomashoneyman commented Apr 25, 2018

Compatible with and already merged with #35

@thomashoneyman

This comment has been minimized.

Copy link
Member

thomashoneyman commented Apr 27, 2018

@crcornwell is working on a way to generate the updateValue / updateValidate functions with RowList. See the relevant discussion here:

https://purescript-users.ml/t/generating-lenses-from-variants/54/9

I'm working on a class to generate the initial form state. With these in place, the boilerplate issue is pretty much solved.

thomashoneyman added some commits Apr 27, 2018

@thomashoneyman

This comment has been minimized.

Copy link
Member

thomashoneyman commented Apr 28, 2018

Latest commits have eliminated the boilerplate around generating initial form state and the form inputs. All that remains is to get rid of the updateValue / updateValidate boilerplate, which @crcornwell ought to have completed by end of day today.

Once that's in, I'll add a snippet of code creating the form without comments.

crcornwell and others added some commits Apr 28, 2018

@thomashoneyman

This comment has been minimized.

Copy link
Member

thomashoneyman commented Apr 28, 2018

This is what creating a form might look like:

signupForm ::  eff. SignupForm (Aff eff)
signupForm = { email: _, password: _ }
  <$> emailForm
  <*> passwordForm
  where
    emailForm = formFromField _email $
      hoistFnV validateNonEmptyStr
      >>> hoistFnV (validateStrIsEmail "Not a valid email address.")

    passwordForm = ( { p1: _, p2: _ }
      <$> formFromField _p1 (hoistFnV validateNonEmptyStr)
      <*> formFromField _p2 (hoistFnV validateNonEmptyStr)
      )
      >>> hoistFnV \{ p1, p2 } -> collapseIfEqual p1 p2 _p2

signupInitialForm :: FormInputs
signupInitialForm = makeDefaultFormInputs (RProxy :: RProxy (FormFieldsT Second))

type FormFieldsT f =
  ( email :: f EmailError      String String
  , p1    :: f PasswordError   String String
  , p2    :: f PasswordErrorEq String String
  )

_email = SProxy :: SProxy "email"
_p1    = SProxy :: SProxy "p1"
_p2    = SProxy :: SProxy "p2"

type FieldValueV    = Variant (FormFieldsT Second)
type FieldValidateV = Variant (FormFieldsT (K Boolean))

_form = SProxy :: SProxy "form"

updateValue :: FieldValueV -> (State -> State)
updateValue = modify _form <<< valueSetter (RProxy :: RProxy (FormFieldsT Second)) case_

updateValidate :: FieldValidateV -> (State -> State)
updateValidate = modify _form <<< validateSetter (RProxy :: RProxy (FormFieldsT (K Boolean))) case_

type FormInputs = Record (FormFieldsT (FormInput' FieldValueV FieldValidateV))

type FormFieldsOutT f =
  ( email    :: f EmailError    String String
  , password :: f PasswordError String String
  )
type FormOutputs = Record (FormFieldsOutT FormMaybe')

type SignupForm m = Form m FormInputs FormOutputs

Routed through a component like this:

data Query a
  = UpdateContents FieldValueV a
  | ValidateOne FieldValidateV a
  | ValidateAll a

type State =
  { form :: FormInputs
  , result :: Maybe { email :: String, password :: String }
  }

component = ...
  initialState :: State
  initialState = { form: signupInitialForm, result: Nothing }

  eval :: Query ~> H.ComponentDSL State Query Void (Aff ( console :: CONSOLE | eff))
  eval = case _ of
    UpdateContents val next -> do
      H.modify $ updateValue val
      pure next

    ValidateOne val next -> do
      H.modify $ updateValidate val
      eval $ ValidateAll next

    ValidateAll next -> do
      st <- H.get
      (Tuple form result) <- H.liftAff do
         res <- runForm signupForm st.form
         case res of
           Valid form value -> do
             pure $ Tuple form value
           Invalid form -> do
             pure $ Tuple form Nothing
      H.modify _ { form = form, result = result }
      pure next
@thomashoneyman

This comment has been minimized.

Copy link
Member

thomashoneyman commented Apr 28, 2018

@crcornwell @whoadave @foresttoney

This branch is no longer a breaking change. Wildcat has been updated for compatibility and this can be merged into dev on both sides.

We should review on Monday and merge by EOD.

@@ -1,39 +1,24 @@
module Ocelot.Core.Utils
( blockBuilder
module Ocelot.Properties

This comment has been minimized.

@crcornwell

crcornwell Apr 30, 2018

Contributor

Ocelot.HTML.Properties?

, setValue :: vl
, setValidate :: vd
, shouldValidate :: Boolean
}

This comment has been minimized.

@crcornwell

crcornwell Apr 30, 2018

Contributor

value -> input
validated -> result
setValue -> setInput
shouldValidate -> validate

vl -> iv
vd -> vv

@thomashoneyman

This comment has been minimized.

Copy link
Member

thomashoneyman commented Apr 30, 2018

@crcornwell Need to verify that the name changes won't affect anything in Wildcat. I expect renaming Properties will require the same change there.

@crcornwell crcornwell merged commit 100d872 into dev May 1, 2018

1 check passed

ci/circleci: test Your tests passed on CircleCI!
Details

@crcornwell crcornwell deleted the polyform branch May 1, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment