{"id":13682404,"url":"https://github.com/thomashoneyman/purescript-halogen-formless","last_synced_at":"2026-02-11T16:35:07.245Z","repository":{"id":38858508,"uuid":"139774364","full_name":"thomashoneyman/purescript-halogen-formless","owner":"thomashoneyman","description":"Forms for Halogen","archived":false,"fork":false,"pushed_at":"2023-08-02T22:53:48.000Z","size":1395,"stargazers_count":137,"open_issues_count":2,"forks_count":31,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-08-07T20:26:49.524Z","etag":null,"topics":["form","halogen","halogen-hooks","purescript"],"latest_commit_sha":null,"homepage":"https://thomashoneyman.github.io/purescript-halogen-formless","language":"PureScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thomashoneyman.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null},"funding":{"github":["thomashoneyman"]}},"created_at":"2018-07-04T23:45:27.000Z","updated_at":"2025-06-26T06:35:11.000Z","dependencies_parsed_at":"2024-01-08T14:30:48.205Z","dependency_job_id":"ab823c1c-ccbd-4439-95af-c67fe38c7fc1","html_url":"https://github.com/thomashoneyman/purescript-halogen-formless","commit_stats":{"total_commits":199,"total_committers":13,"mean_commits":"15.307692307692308","dds":"0.14070351758793975","last_synced_commit":"2d195d2e632c40bb8e316197e53a389cbc24bf5f"},"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/thomashoneyman/purescript-halogen-formless","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-formless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-formless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-formless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-formless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thomashoneyman","download_url":"https://codeload.github.com/thomashoneyman/purescript-halogen-formless/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomashoneyman%2Fpurescript-halogen-formless/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271785845,"owners_count":24820553,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-24T02:00:11.135Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["form","halogen","halogen-hooks","purescript"],"created_at":"2024-08-02T13:01:45.604Z","updated_at":"2026-02-11T16:35:02.215Z","avatar_url":"https://github.com/thomashoneyman.png","language":"PureScript","readme":"# Formless\n\n[![CI](https://github.com/thomashoneyman/purescript-halogen-formless/workflows/CI/badge.svg?branch=main)](https://github.com/thomashoneyman/purescript-halogen-formless/actions?query=workflow%3ACI+branch%3Amain)\n[![Latest release](http://img.shields.io/github/release/thomashoneyman/purescript-halogen-formless.svg)](https://github.com/thomashoneyman/purescript-halogen-formless/releases)\n[![Maintainer: thomashoneyman](https://img.shields.io/badge/maintainer-thomashoneyman-teal.svg)](http://github.com/thomashoneyman)\n\nFormless helps you write forms in Halogen without the boilerplate.\n\n- [Examples \u0026 documentation site](https://thomashoneyman.github.io/purescript-halogen-formless/)\n- [Source code for examples](./example)\n- [Migration of Real World Halogen from Formless 2 to Formless 3](https://github.com/thomashoneyman/purescript-halogen-realworld/pull/102)\n\n## Installation\n\nInstall Formless with Spago:\n\n```console\n$ spago install halogen-formless\n```\n\nFormless 3 is available in package sets beginning with `psc-0.14.7-20220303`. If you are using a package set that does not include Formless, then you can add it to your local set as shown in the example below:\n\n```dhall\nlet upstream = ...\n\nin  upstream\n  with halogen-formless =\n    { version = \"v3.0.0\"\n    , repo = \"https://github.com/thomashoneyman/purescript-halogen-formless.git\"\n    , dependencies =\n        [ \"convertable-options\"\n        , \"effect\"\n        , \"either\"\n        , \"foldable-traversable\"\n        , \"foreign-object\"\n        , \"halogen\"\n        , \"heterogeneous\"\n        , \"maybe\"\n        , \"prelude\"\n        , \"record\"\n        , \"safe-coerce\"\n        , \"type-equality\"\n        , \"unsafe-coerce\"\n        , \"unsafe-reference\"\n        , \"variant\"\n        , \"web-events\"\n        , \"web-uievents\"\n        ]\n    }\n```\n\n## Tutorial\n\nWe're going to write a form from scratch, demonstrating how to use Formless with no helper functions. This tutorial can serve as the basis for your real applications, but you'll typically write your own helper functions for common form controls and validation in your app. Make sure to check out the [examples directory](./example) after you read this tutorial to expand your knowledge!\n\nOur form will let a user register their cat for pet insurance by recording its name, nickname, and age. Let's take the first step!\n\n### Define a form type\n\nWe'll start by defining a type for our form.\n\n```purs\ntype Form :: (Type -\u003e Type -\u003e Type -\u003e Type) -\u003e Row Type\ntype Form f =\n  ( name     :: f String String String\n  , nickname :: f String Void   (Maybe String)\n  , age      :: f String String Int\n  --              input  error  output\n  )\n```\n\nForm types are typically defined as a row of form fields, where each form field specifies its input, error, and output type as arguments to `f`.\n\n- The `input` type describes what the form field will receive from the user. For example, a text input will receive a `String`, while a radio group might use a custom sum type.\n- The `error` type describes what validation errors can occur for this form field. We'll stick to `String` for our example, but you can create your own form- or app-specific error types.\n- The `output` type describes what our input type will parse to, if it passes validation. For example, while we'll let the user type their cat's age into a text field and therefore accept a `String` as input, in our application we will only consider `Int` ages to be valid.\n\nTake a moment and think about what the input, error, and output types for each of our three fields are. Our `nickname` field has an output type of `Maybe String` -- what do you think that represents?\n\nDefining our form row this way provides maximum flexibility for defining other type synonyms in terms of the form row. This greatly reduces the amount of code you need to write for your form. For example, Formless requires that we provide an initial set of values for our form fields:\n\n```purs\ninitialValues = { name: \"\", nickname: \"\", age: \"\" }\n```\n\nWe can write a type for this value by writing a brand new record type, or by reusing our form type:\n\n```purs\nimport Formless as F\n\n-- Option 1: Define a new record type\ntype FormInputs = { name :: String, nickname :: String, age :: String }\n\n-- Option 2: Reuse our form row\ntype FormInputs = { | Form F.FieldInput }\n```\n\nThese two implementations of `FormInputs` are identical. However, reusing the form row requires less typing and ensures a single source of truth.\n\n### Write component types\n\nFormless is a higher-order component, which means that it takes a component as an argument and returns a new component. The returned component can have any input, query, output, and monad types you wish -- Formless is entirely transparent from the perspective of a parent component.\n\n#### Public Types\n\nLet's write concrete types for our component's public interface. We don't need any input or to handle any queries, but we'll have our form raise a custom success message and a valid `Cat` as its output.\n\n```purs\n-- Reusing our form row again! This type is identical to:\n-- { name :: String, nickname :: Maybe String, age :: Int }\ntype Cat = { | Form F.FieldOutput }\n\ntype Query = Const Void\n\ntype Input = Unit\n\ntype Output = { successMessage :: String, newCat :: Cat }\n\n-- We now have the types necessary for our wrapped component,\n-- which we'll run in `Aff`:\ncomponent :: H.Component Query Input Output Aff\n```\n\n#### Internal Types\n\nNext, we'll turn to our internal component types: the state and action types (we don't need any child slots, so we'll hard code them to `()`).\n\nFormless requires our component to support two actions:\n\n- Your component must receive input of type `FormContext`, which includes the form fields and useful actions for controlling the form. It also includes any other input you want your component to take. By convention this action is called `Receive`.\n- Your component must raise actions of type `FormlessAction` to Formless for evaluation. By convention this action is called `Eval`.\n\nThe `FormContext` and `FormlessAction` types you need to write for your `Action` type can be easily implemented by reusing your form row along with type synonyms provided by Formless. Let's define these two types for our form:\n\n```purs\n-- Our form will receive `FormContext` as input. We can specialize the Formless-\n-- provided `F.FormContext` type to our form by giving it our form row applied\n-- to the `F.FieldState` and `F.FieldAction` type synonyms.\n--\n-- The form context includes the current state of all fields in the form, so its\n-- first argument is our form row applied to `F.FieldState`. It also includes a\n-- set of actions for controlling the form, so our second argument is our form\n-- row and component action type applied to `F.FieldAction`. Finally, the form\n-- context passes through the input type we already defined for our component\n-- (in our case, `Unit`), and so it takes the `Input` type as its third\n-- argument. Finally, it provides some form-wide helper actions, and so we must\n-- provide our `Action` type as the fourth argument.\ntype FormContext = F.FormContext (Form F.FieldState) (Form (F.FieldAction Action)) Input Action\n\n-- Our form raises Formless actions for evaluation, most of which track the\n-- state of a particular form field. We can specialize `F.FormlessAction` to our\n-- form by giving it our form row applied to the `F.FieldState` type synonym.\ntype FormlessAction = F.FormlessAction (Form F.FieldState)\n```\n\nWith our `FormContext` and `FormlessAction` types specialized, we can now implement our component's internal `Action` type:\n\n```purs\ndata Action\n  = Receive FormContext\n  | Eval FormlessAction\n```\n\nThe `FormContext` and `FormlessAction` types can be confusing the first time you see them. If they are a lot to take in, don't worry: you'll get used to them, and after you define them once you don't have to touch them again (any changs you make to your form will happen on the form row).\n\nOur final component type is the `State` type. We don't need any extra state beyond what Formless gives us, so we'll just reuse the `FormContext` as our state type:\n\n```purs\ntype State = FormContext\n```\n\n### Implement your form component\n\nWe can now write our form component and make use of the state and helper functions that Formless makes available to us.\n\nYou will typically implement your form component by applying Formless directly to `H.mkComponent`, which saves quite a bit of typing. The Formless higher-order component takes three arguments:\n\n- A `FormConfig`, which lets you control some of Formless' behavior, like when validation should be run, and lets you lift Formless actions into your `Action` type. The only required option is `liftAction`; all other fields are entirely optional.\n- A record of initial values for each field in your form. We already wrote an `initialValues` when we defined our form type, but since all our inputs are strings, we could also implement our initial form as a simple `mempty`. This is what's demonstrated below.\n- Your form component, which must accept `FormContext` as input, handle queries of type `FormQuery`, and raise outputs of type `FormOutput`. Don't worry -- we'll talk more about each of these!\n\n```purs\nimport Halogen as H\nimport Effect.Aff (Aff)\nimport Data.Maybe (Maybe(..))\n\nform :: H.Component Query Input Output Aff\nform = F.formless { liftAction: Eval } mempty $ H.mkComponent\n  { initialState: \\context -\u003e context\n  , render\n  , eval: H.mkEval $ H.defaultEval\n      { receive = Just \u003c\u003c\u003c Receive\n      , handleAction = handleAction\n      , handleQuery = handleQuery\n      }\n  }\n```\n\n#### Rendering Your Form\n\nThe Formless form context provides you with the state of each field in your form, along with pre-made actions for handling change, blur, and other events. You can use this information to implement a basic form.\n\nIn the below example, we make use of a form-wide action (`handleSubmit`), field-specific actions (`handleChange`, `handleBlur`), and field-specific state (`value`, `result`).\n\n```purs\nform = F.formless ...\n  where\n  render :: FormContext -\u003e H.ComponentHTML Action () Aff\n  render { formActions, fields, actions } =\n    HH.form\n      [ HE.onSubmit formActions.handleSubmit ]\n      [ HH.div_\n          [ HH.label_\n              [ HH.text \"Name\" ]\n          , HH.input\n              [ HP.type_ HP.InputText\n              , HP.placeholder \"Scooby\"\n              , HP.value fields.name.value\n              , HE.onValueInput actions.name.handleChange\n              , HE.onBlur actions.name.handleBlur\n              ]\n            -- We can use the `result` field to check if we have an error\n          , case fields.name.result of\n              Just (Left error) -\u003e HH.text error\n              _ -\u003e HH.text \"\"\n          ]\n      ]\n```\n\nIt's tedious and error-prone manually wiring up form fields, so most applications should define their own reusable form controls by abstracting what you see here. You can see examples of that in the [examples directory](./example).\n\n#### Handling Actions\n\nEvery form component you provide to Formless should implement a `handleAction` function that updates your component when new form context is provided and tells Formless to evaluate form actions when they arise in your component. A typical `handleAction` function in a form component looks like this:\n\n```purs\nform = F.formless ...\n  where\n  -- Here we've written out the full type signature for `handleAction`, but the\n  -- compiler can infer these types for you if you would like to omit the type\n  -- signature or provide `_` wildcards for lengthy types like `F.FormOutput`.\n  --\n  -- Remember that our outer component has an output type of `Output`, but our\n  -- inner component raises messages to Formless rather than to the form parent\n  -- directly. We raise both our own output messages, `Output`, and also Formless\n  -- actions that need to be evaluated. For that reason, we use the `F.FormOutput`\n  -- output type for our inner component.\n  handleAction\n    :: Action\n    -\u003e H.HalogenM State Action () (F.FormOutput (Form F.FieldState) Output) Aff Unit\n  handleAction = case _ of\n    -- When we receive new form context we need to update our form state.\n    Receive context -\u003e\n      H.put context\n\n    -- When a `FormlessAction` has been triggered we must raise it up to\n    -- Formless for evaluation. We can do this with `F.eval`.\n    Eval action -\u003e\n      F.eval action\n```\n\nYou can freely add your own actions to your form for anything else your form needs to do. See the [examples](./example) for...examples!\n\n#### Handling Queries\n\nFormless uses queries to notify your form component of important events like when a form is submitted or reset, or when a form field needs to be validated.\n\nUnlike previous versions of Formless, you don't provide any validation functions to the form directly. Instead, you will receive a `Validate` query that contains an input from your form. You are required to return an `Either error output` for that field back to Formless.\n\nThe most important benefit of this approach is that you can write validation functions that run in the context of your form component. That means that your validators can freely access your form state, including the state of other fields in the form, and you can evaluate actions in your component as part of validation (for example, making a request or setting the value of another field). We'll just explore pure validation in this example, but the [examples directory](./example) demonstrates various validation scenarios.\n\nA typical `handleQuery` function uses the `handleSubmitValidate` or `handleSubmitValidateM` helper functions to only deal with form submission and validation events. In our case, we'll simply raise a successful form submission as output, and we'll provide a set of pure validation functions:\n\n```purs\nform = F.formless ...\n  where\n  -- Here we'll use wildcards rather than type everything out; the compiler is\n  -- able to infer these types for us.\n  handleQuery :: forall a. F.FormQuery _ _ _ _ a -\u003e H.HalogenM _ _ _ _ _ (Maybe a)\n  handleQuery = do\n    let\n      -- These validators would usually be in a separate validation module in\n      -- your app rather than be defined inline like this.\n      validateName :: String -\u003e Either String String\n      validateName input\n        | input == \"\" = Left \"Required\"\n        | otherwise = Right input\n\n      validateNickname :: String -\u003e Either Void (Maybe String)\n      validateNickname input\n        | input == \"\" = Right Nothing\n        | otherwise = Right (Just input)\n\n      validateAge :: String -\u003e Either String Int\n      validateAge input = case Int.fromString input of\n        Nothing -\u003e Left \"Not a valid integer.\"\n        Just n\n          | n \u003e 20 -\u003e Left \"No dog is over 20 years old!\"\n          | n \u003c= 0 -\u003e Left \"No dog is less than 0 years old!\"\n          | otherwise -\u003e Right n\n\n      validation :: { | Form F.FieldValidation }\n      validation =\n        { name: validateName\n        , nickname: validateNickname\n        , age: validateAge\n        }\n\n      handleSuccess :: Cat -\u003e H.HalogenM _ _ _ _ _ Unit\n      handleSuccess cat = do\n        let\n          output :: Output\n          output = { successMessage: \"Got a cat!\", newCat: cat }\n\n        -- F.raise is a helper function for raising your `Output` type through\n        -- Formless and up to the parent component.\n        F.raise output\n\n    -- handleSubmitValidate lets you provide a success handler and a record\n    -- of validation functions to handle submission and validation events.\n    F.handleSubmitValidate handleSuccess F.validate validation\n```\n\nIn a typical form, you wouldn't write out all these types, and your validation functions would probably live in a separate `Validation` module in your project. In the real world, a more typical `handleQuery` looks like this:\n\n```purs\nimport MyApp.Validation as V\n\nform = F.formless ...\n  where\n  handleQuery :: forall a. F.FormQuery _ _ _ _ a -\u003e H.HalogenM _ _ _ _ _ (Maybe a)\n  handleQuery = F.handleSubmitValidate F.raise F.validate\n    { name: V.required\n    , nickname: V.optional\n    , age: V.int \u003e=\u003e V.greaterThan 0 \u003e=\u003e V.lessThan 20\n    }\n```\n\nIf you would like to see all possible events that your `handleQuery` function can handle, please see the implementation of `handleSubmitValidate`.\n\n## Comments \u0026 Improvements\n\nHave any comments about the library or any ideas to improve it for your use case? Please file an issue, or reach out on the [PureScript forum](https://discourse.purescript.org) or [PureScript chat](https://purescript.org/chat).\n","funding_links":["https://github.com/sponsors/thomashoneyman"],"categories":["PureScript","Components"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomashoneyman%2Fpurescript-halogen-formless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthomashoneyman%2Fpurescript-halogen-formless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomashoneyman%2Fpurescript-halogen-formless/lists"}