diff --git a/example/external-components/Component.purs b/example/external-components/Component.purs index 73a76ff..33a6b74 100644 --- a/example/external-components/Component.purs +++ b/example/external-components/Component.purs @@ -9,7 +9,7 @@ import Effect.Console as Console import Example.App.UI.Element as UI import Example.App.UI.Typeahead as TA import Example.ExternalComponents.RenderForm (formless) -import Example.ExternalComponents.Spec (User, _email, _language, _whiskey, formSpec, submitter, validator) +import Example.ExternalComponents.Spec (User, proxies, formSpec, submitter, validator) import Example.ExternalComponents.Types (ChildQuery, ChildSlot, Query(..), Slot(..), State) import Formless as F import Halogen as H @@ -72,13 +72,13 @@ component = Typeahead slot (TA.SelectionsChanged new) a -> case slot of Email -> a <$ do - H.query unit $ H.action $ F.ModifyValidate (F.setInput _email new) + H.query unit $ H.action $ F.ModifyValidate (F.setInput proxies.email new) Whiskey -> a <$ do - _ <- H.query unit $ H.action $ F.ModifyValidate (F.setInput _whiskey new) + _ <- H.query unit $ H.action $ F.ModifyValidate (F.setInput proxies.whiskey new) -- We'll clear the email field when a new whiskey is selected - _ <- H.query unit $ H.action $ F.Reset (F.resetField _email) + _ <- H.query unit $ H.action $ F.Reset (F.resetField proxies.email) H.query unit $ H.action $ F.Send Email (H.action TA.Clear) Language -> a <$ do - H.query unit $ H.action $ F.ModifyValidate (F.setInput _language new) + H.query unit $ H.action $ F.ModifyValidate (F.setInput proxies.language new) diff --git a/example/external-components/RenderForm.purs b/example/external-components/RenderForm.purs index 6ec9169..137c097 100644 --- a/example/external-components/RenderForm.purs +++ b/example/external-components/RenderForm.purs @@ -5,7 +5,7 @@ import Prelude import Effect.Aff (Aff) import Example.App.UI.Element as UI import Example.App.UI.Typeahead as TA -import Example.ExternalComponents.Spec (Form, User, _email, _language, _name, _whiskey) +import Example.ExternalComponents.Spec (Form, User, proxies) import Example.ExternalComponents.Types (Query(..), Slot(..)) import Formless as F import Halogen as H @@ -21,7 +21,7 @@ formless state = { label: "Name" , help: "Write your name" , placeholder: "Dale" - , sym: _name + , sym: proxies.name } state , email state , whiskey state @@ -55,7 +55,7 @@ email :: F.State Form User Aff -> F.HTML Query (TA.Query String) Slot Form User email state = UI.field { label: "Email" - , help: UI.resultToHelp "Choose an email address -- carefully." (F.getResult _email state.form) + , help: UI.resultToHelp "Choose an email address -- carefully." (F.getResult proxies.email state.form) } [ HH.slot Email TA.single { placeholder: "me@you.com" @@ -74,7 +74,7 @@ whiskey :: F.State Form User Aff -> F.HTML Query (TA.Query String) Slot Form Use whiskey state = UI.field { label: "Whiskey" - , help: UI.resultToHelp "Select a favorite whiskey" (F.getResult _whiskey state.form) + , help: UI.resultToHelp "Select a favorite whiskey" (F.getResult proxies.whiskey state.form) } [ HH.slot Whiskey TA.single { placeholder: "Lagavulin 12" @@ -92,7 +92,7 @@ language :: F.State Form User Aff -> F.HTML Query (TA.Query String) Slot Form Us language state = UI.field { label: "Language" - , help: UI.resultToHelp "Choose your favorite programming language." (F.getResult _language state.form) + , help: UI.resultToHelp "Choose your favorite programming language." (F.getResult proxies.language state.form) } [ HH.slot Language TA.single { placeholder: "Haskell" diff --git a/example/external-components/Spec.purs b/example/external-components/Spec.purs index c712352..9faea00 100644 --- a/example/external-components/Spec.purs +++ b/example/external-components/Spec.purs @@ -4,12 +4,10 @@ import Prelude import Data.Maybe (Maybe, fromMaybe) import Data.Newtype (class Newtype) -import Data.Symbol (SProxy(..)) import Example.App.Validation as V -import Formless.Spec (FormSpec, InputType, InputField, OutputType, OutputField) -import Formless.Spec.Transform (mkFormSpecFromRow, unwrapOutput) +import Formless.Spec (FormProxy(..), FormSpec, InputField, OutputField, OutputType) +import Formless.Spec.Transform (SProxies, mkFormSpecFromProxy, mkSProxies, unwrapOutput) import Formless.Validation.Semigroup (applyOnInputFields) -import Type.Row (RProxy(..)) type User = Record (FormRow OutputType) @@ -24,13 +22,11 @@ type FormRow f = ) -- | You'll usually want symbol proxies for convenience -_name = SProxy :: SProxy "name" -_email = SProxy :: SProxy "email" -_whiskey = SProxy :: SProxy "whiskey" -_language = SProxy :: SProxy "language" +proxies :: SProxies Form +proxies = mkSProxies $ FormProxy :: FormProxy Form formSpec :: Form FormSpec -formSpec = mkFormSpecFromRow $ RProxy :: RProxy (FormRow InputType) +formSpec = mkFormSpecFromProxy $ FormProxy :: FormProxy Form validator :: Form InputField -> Form InputField validator = applyOnInputFields diff --git a/example/polyform/Component.purs b/example/polyform/Component.purs index 504125b..ea4cdd2 100644 --- a/example/polyform/Component.purs +++ b/example/polyform/Component.purs @@ -2,13 +2,13 @@ module Example.Polyform.Component where import Prelude -import Example.App.UI.Element as UI import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype) import Data.Symbol (SProxy(..)) import Effect.Aff (Aff) import Effect.Class (class MonadEffect) import Effect.Console as Console +import Example.App.UI.Element as UI import Example.App.Validation as V import Formless as F import Formless.Validation.Polyform (applyOnInputFields) @@ -18,7 +18,6 @@ import Halogen.HTML.Events as HE import Halogen.HTML.Properties as HP import Polyform.Validation as Validation import Record (delete) -import Type.Row (RProxy(..)) data Query a = HandleFormless (F.Message' Form User) a @@ -72,7 +71,7 @@ component = , HH.slot unit F.component - { formSpec: F.mkFormSpecFromRow $ RProxy :: RProxy (FormRow F.InputType) + { formSpec: F.mkFormSpecFromProxy _form , validator , submitter: pure <<< F.unwrapOutput , render: renderFormless @@ -84,11 +83,22 @@ component = ---------- -- Formless +-- We can recover both our user type and our form from the same row. type User = Record (FormRow F.OutputType) newtype Form f = Form (Record (FormRow f)) derive instance newtypeForm :: Newtype (Form f) _ +-- This proxy will let us generate all the SProxies for our form as +-- well as our entire initial form. +_form = F.FormProxy :: F.FormProxy Form + +-- This is a record of symbol proxies, which we can now pass to the +-- various Formless functions that require them. See the render function +-- below as an example in practice. +proxies :: F.SProxies Form +proxies = F.mkSProxies _form + type FormRow f = ( name :: f V.Errs String V.Name , email :: f V.Errs String V.Email @@ -96,11 +106,6 @@ type FormRow f = , state :: f V.Errs String String ) -_name = SProxy :: SProxy "name" -_email = SProxy :: SProxy "email" -_city = SProxy :: SProxy "city" -_state = SProxy :: SProxy "state" - validator :: ∀ m. MonadEffect m => Form F.InputField -> m (Form F.InputField) validator = applyOnInputFields { name: V.Name <$> (V.minLength 5 *> V.maxLength 10) @@ -117,28 +122,28 @@ renderFormless state = { label: "Name" , help: "Write your name" , placeholder: "Dale" - , sym: _name + , sym: proxies.name } state , UI.formlessField UI.input { label: "Email Address" , help: "Write your email" , placeholder: "me@you.com" - , sym: _email + , sym: proxies.email } state , UI.formlessField UI.input { label: "City" , help: "Write your favorite city" , placeholder: "Los Angeles" - , sym: _city + , sym: proxies.city } state , UI.formlessField UI.input { label: "State" , help: "Write your favorite state of mind" , placeholder: "" - , sym: _state + , sym: proxies.state } state , HH.br_ , UI.p_ $ @@ -162,3 +167,4 @@ renderFormless state = [ HH.text "Reset" ] ] ] + diff --git a/example/real-world/Component.purs b/example/real-world/Component.purs index 037c758..da87ef1 100644 --- a/example/real-world/Component.purs +++ b/example/real-world/Component.purs @@ -10,8 +10,7 @@ import Example.App.UI.Dropdown as DD import Example.App.UI.Element (css) import Example.App.UI.Element as UI import Example.App.UI.Typeahead as TA -import Example.RealWorld.Data.Group (Group(..), _admin, _applications, _pixels, _secretKey1, _secretKey2, _whiskey) -import Example.RealWorld.Data.Options (Options(..), _enable, _metric) +import Example.RealWorld.Data.Group as G import Example.RealWorld.Data.Options as O import Example.RealWorld.Render.GroupForm as GroupForm import Example.RealWorld.Render.OptionsForm as OptionsForm @@ -109,7 +108,7 @@ component = F.component { formSpec: defaultOptionsSpec , validator: pure <$> optionsFormValidate - , submitter: pure <<< Options <<< F.unwrapOutput + , submitter: pure <<< O.Options <<< F.unwrapOutput , render: OptionsForm.render } (HE.input OptionsForm) @@ -143,7 +142,7 @@ component = -- Here, we'll construct our new group from the two form outputs. case mbGroupForm, mbOptionsForm of Just g, Just v -> do - H.modify_ _ { group = map (over Group (_ { options = v })) g } + H.modify_ _ { group = map (over G.Group (_ { options = v })) g } _, _ -> H.liftEffect (Console.error "Forms did not validate.") st <- H.get H.liftEffect $ Console.log $ show st.group @@ -163,22 +162,22 @@ component = pure a TASingle (TA.SelectionsChanged new) a -> a <$ do - H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput _whiskey new) + H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput G.proxies.whiskey new) TAMulti slot (TA.SelectionsChanged new) a -> a <$ case slot of Applications -> - H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput _applications new) + H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput G.proxies.applications new) Pixels -> - H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput _pixels new) + H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput G.proxies.pixels new) AdminDropdown m a -> a <$ do - _ <- H.query' CP.cp1 unit $ H.action $ F.Reset (F.resetField _secretKey1) - _ <- H.query' CP.cp1 unit $ H.action $ F.Reset (F.resetField _secretKey2) + _ <- H.query' CP.cp1 unit $ H.action $ F.Reset (F.resetField G.proxies.secretKey1) + _ <- H.query' CP.cp1 unit $ H.action $ F.Reset (F.resetField G.proxies.secretKey2) case m of DD.Selected x -> do - H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput _admin (Just x)) + H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput G.proxies.admin (Just x)) DD.Cleared -> do - H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput _admin Nothing) + H.query' CP.cp1 unit $ H.action $ F.ModifyValidate (F.setInput G.proxies.admin Nothing) ----- -- Options Form @@ -191,10 +190,10 @@ component = st' <- H.modify _ { optionsFormErrors = fstate.errors , optionsFormDirty = fstate.dirty - , optionsEnabled = F.getInput _enable fstate.form + , optionsEnabled = F.getInput O.proxies.enable fstate.form } - let submitter = pure <<< Options <<< F.unwrapOutput + let submitter = pure <<< O.Options <<< F.unwrapOutput validator = pure <$> optionsFormValidate -- The generated spec will set enabled to false, but we'll want it to be true before @@ -212,6 +211,6 @@ component = MetricDropdown m a -> a <$ case m of DD.Selected x -> do - H.query' CP.cp2 unit $ H.action $ F.ModifyValidate (F.setInput _metric (Just x)) + H.query' CP.cp2 unit $ H.action $ F.ModifyValidate (F.setInput O.proxies.metric (Just x)) DD.Cleared -> do - H.query' CP.cp2 unit $ H.action $ F.ModifyValidate (F.setInput _metric Nothing) + H.query' CP.cp2 unit $ H.action $ F.ModifyValidate (F.setInput O.proxies.metric Nothing) diff --git a/example/real-world/Data/Group.purs b/example/real-world/Data/Group.purs index a0ff62d..c146ead 100644 --- a/example/real-world/Data/Group.purs +++ b/example/real-world/Data/Group.purs @@ -41,12 +41,6 @@ type GroupRow f r = | r ) -_name = SProxy :: SProxy "name" -_admin = SProxy :: SProxy "admin" -_applications = SProxy :: SProxy "applications" -_pixels = SProxy :: SProxy "pixels" -_whiskey = SProxy :: SProxy "whiskey" - -- | Here's the Group data type we'll use throughout our application. After we send -- | a form result off to the server, this is what we'll get in return. newtype Group = Group @@ -70,12 +64,12 @@ _options = SProxy :: SProxy "options" newtype GroupForm f = GroupForm (Record (GroupFormRow f)) derive instance newtypeGroupForm :: Newtype (GroupForm f) _ +proxies :: F.SProxies GroupForm +proxies = F.mkSProxies $ F.FormProxy :: F.FormProxy GroupForm + -- | In order to generate our fields automatically using mkFormSpecFromRow, we'll make -- | sure to have the new row as a new type. type GroupFormRow f = GroupRow f ( secretKey1 :: f Errs String String , secretKey2 :: f Errs String String ) - -_secretKey1 = SProxy :: SProxy "secretKey1" -_secretKey2 = SProxy :: SProxy "secretKey2" diff --git a/example/real-world/Data/Options.purs b/example/real-world/Data/Options.purs index e5f0ed7..1c4235f 100644 --- a/example/real-world/Data/Options.purs +++ b/example/real-world/Data/Options.purs @@ -8,10 +8,8 @@ import Prelude import Data.Generic.Rep (class Generic) import Data.Generic.Rep.Show (genericShow) -import Data.Maybe (Maybe(..)) +import Data.Maybe (Maybe) import Data.Newtype (class Newtype) -import Data.String.Read (class Read) -import Data.Symbol (SProxy(..)) import Example.App.Validation (class ToText, Errs) import Formless as F @@ -44,12 +42,6 @@ instance toTextMetric :: ToText Metric where toText ClickCost = "Click Cost" toText InstallCost = "Install Cost" -instance readMetric :: Read Metric where - read "View Cost" = Just ViewCost - read "Click Cost" = Just ClickCost - read "Install Cost" = Just InstallCost - read _ = Nothing - -- | This data type will be used in radio buttons, and so if we -- | want to generate an initial form from our row, we'll need an -- | instance of the F.Initial type class @@ -85,14 +77,8 @@ type OptionsRow f = , speed :: f Unit Speed Speed ) -_enable = SProxy :: SProxy "enable" -_metric = SProxy :: SProxy "metric" -_viewCost = SProxy :: SProxy "viewCost" -_clickCost = SProxy :: SProxy "clickCost" -_installCost = SProxy :: SProxy "installCost" -_size = SProxy :: SProxy "size" -_dimensions = SProxy :: SProxy "dimensions" -_speed = SProxy :: SProxy "speed" +proxies :: F.SProxies OptionsForm +proxies = F.mkSProxies $ F.FormProxy :: F.FormProxy OptionsForm -- | This is the data type used throughout the application. In this case, it's the same -- | as the form and the underlying row. diff --git a/example/real-world/Render/GroupForm.purs b/example/real-world/Render/GroupForm.purs index aacde80..00a6895 100644 --- a/example/real-world/Render/GroupForm.purs +++ b/example/real-world/Render/GroupForm.purs @@ -7,7 +7,7 @@ import Effect.Aff (Aff) import Example.App.UI.Dropdown as Dropdown import Example.App.UI.Element as UI import Example.App.UI.Typeahead as Typeahead -import Example.RealWorld.Data.Group (Admin(..), GroupId(..)) +import Example.RealWorld.Data.Group (Admin(..), GroupId(..), proxies) import Example.RealWorld.Data.Group as G import Example.RealWorld.Types (GroupCQ, GroupCS, GroupTASlot(..), Query(..)) import Formless as F @@ -46,7 +46,7 @@ renderName = { label: "Name" , help: "Give the group a name." , placeholder: "January Analytics Seminar" - , sym: G._name + , sym: proxies.name } renderSecretKey1 :: FormlessState -> FormlessHTML @@ -55,7 +55,7 @@ renderSecretKey1 = { label: "Secret Key 1" , help: "Provide a secret identifier for the group." , placeholder: "iasncat3ihba/0" - , sym: G._secretKey1 + , sym: proxies.secretKey1 } renderSecretKey2 :: FormlessState -> FormlessHTML @@ -64,14 +64,14 @@ renderSecretKey2 = { label: "Secret Key 2" , help: "Confirm the secret identifier for the group." , placeholder: "iasncat3ihba/0" - , sym: G._secretKey2 + , sym: proxies.secretKey2 } renderAdmin :: FormlessState -> FormlessHTML renderAdmin state = UI.field { label: "Administrator" - , help: UI.resultToHelp "Choose an administrator for the account" (F.getResult G._admin state.form) + , help: UI.resultToHelp "Choose an administrator for the account" (F.getResult proxies.admin state.form) } [ HH.slot' CP.cp3 unit Dropdown.component { items, placeholder: "Choose an admin" } @@ -92,7 +92,7 @@ renderWhiskey :: FormlessState -> FormlessHTML renderWhiskey state = UI.field { label: "Whiskey" - , help: UI.resultToHelp "Choose a whiskey to be awarded" (F.getResult G._whiskey state.form) + , help: UI.resultToHelp "Choose a whiskey to be awarded" (F.getResult proxies.whiskey state.form) } [ HH.slot' CP.cp2 unit Typeahead.single { placeholder: "Choose a whiskey" @@ -111,7 +111,7 @@ renderPixels :: FormlessState -> FormlessHTML renderPixels state = UI.field { label: "Tracking Pixels" - , help: UI.resultToHelp "Choose a pixel to track" (F.getResult G._pixels state.form) + , help: UI.resultToHelp "Choose a pixel to track" (F.getResult proxies.pixels state.form) } [ HH.slot' CP.cp1 Pixels Typeahead.multi { placeholder: "Search pixels" @@ -129,7 +129,7 @@ renderApplications :: FormlessState -> FormlessHTML renderApplications state = UI.field { label: "Application Targets" - , help: UI.resultToHelp "Applications are available in several sizes" (F.getResult G._applications state.form) + , help: UI.resultToHelp "Applications are available in several sizes" (F.getResult proxies.applications state.form) } [ HH.slot' CP.cp1 Applications Typeahead.multi { placeholder: "Search one or more applications" diff --git a/example/real-world/Render/OptionsForm.purs b/example/real-world/Render/OptionsForm.purs index 19c1a7d..50e89aa 100644 --- a/example/real-world/Render/OptionsForm.purs +++ b/example/real-world/Render/OptionsForm.purs @@ -10,7 +10,7 @@ import Effect.Aff (Aff) import Example.App.UI.Dropdown as Dropdown import Example.App.UI.Element (css) import Example.App.UI.Element as UI -import Example.RealWorld.Data.Options (Metric(..), Speed(..), _enable, _metric) +import Example.RealWorld.Data.Options (Metric(..), Speed(..), proxies) import Example.RealWorld.Data.Options as OP import Example.RealWorld.Types (OptionsCQ, OptionsCS, Query(..)) import Formless as F @@ -33,7 +33,7 @@ render state = UI.formContent_ [ renderEnabled state , HH.div - [ if F.getInput _enable state.form then css "" else css "is-hidden" ] + [ if F.getInput proxies.enable state.form then css "" else css "is-hidden" ] ( renderMetrics state <> renderOthers state ) @@ -45,7 +45,7 @@ render state = renderMetrics :: FormlessState -> Array FormlessHTML renderMetrics state = [ renderMetric state - , renderMetricField (F.getInput _metric state.form) + , renderMetricField (F.getInput proxies.metric state.form) ] where renderMetricField = case _ of @@ -75,8 +75,8 @@ renderEnabled state = [ HH.input [ css "checkbox" , HP.type_ InputCheckbox - , HP.checked $ F.getInput _enable state.form - , HE.onChange $ HE.input_ $ F.Modify (F.modifyInput _enable not) + , HP.checked $ F.getInput proxies.enable state.form + , HE.onChange $ HE.input_ $ F.Modify (F.modifyInput proxies.enable not) ] , HH.text " Enable extra options" ] @@ -86,7 +86,7 @@ renderMetric :: FormlessState -> FormlessHTML renderMetric state = UI.field { label: "Metric" - , help: UI.resultToHelp "Choose a metric to optimize for." (F.getResult OP._metric state.form) + , help: UI.resultToHelp "Choose a metric to optimize for." (F.getResult proxies.metric state.form) } [ HH.slot unit Dropdown.component { placeholder: "Choose a metric" @@ -101,7 +101,7 @@ renderViewCost = { label: "View Cost" , placeholder: "100" , help: "Enter a dollar amount for view costs." - , sym: OP._viewCost + , sym: proxies.viewCost } renderClickCost :: FormlessState -> FormlessHTML @@ -110,7 +110,7 @@ renderClickCost = { label: "Click Cost" , placeholder: "1" , help: "Enter a dollar amount you're willing to pay for a click." - , sym: OP._clickCost + , sym: proxies.clickCost } renderInstallCost :: FormlessState -> FormlessHTML @@ -119,7 +119,7 @@ renderInstallCost = { label: "Install Cost" , placeholder: "10" , help: "Enter a dollar amount you're willing to pay for an app instal." - , sym: OP._installCost + , sym: proxies.installCost } renderSize :: FormlessState -> FormlessHTML @@ -128,7 +128,7 @@ renderSize = { label: "Size" , placeholder: "10.233" , help: "Enter a total campaign size." - , sym: OP._size + , sym: proxies.size } renderDimensions :: FormlessState -> FormlessHTML @@ -137,7 +137,7 @@ renderDimensions = { label: "Dimensions" , placeholder: "1.027" , help: "Enter a total campaign dimension set ratio buzzword." - , sym: OP._dimensions + , sym: proxies.dimensions } renderSpeed :: FormlessState -> FormlessHTML @@ -153,7 +153,7 @@ renderSpeed state = , css "radio" , HP.type_ InputRadio , HP.checked $ speed.input == Low - , HE.onClick $ HE.input_ $ F.Modify $ F.setInput OP._speed Low + , HE.onClick $ HE.input_ $ F.Modify $ F.setInput proxies.speed Low ] , HH.text $ " " <> show Low ] @@ -164,7 +164,7 @@ renderSpeed state = , css "radio" , HP.type_ InputRadio , HP.checked $ speed.input == Medium - , HE.onClick $ HE.input_ $ F.Modify $ F.setInput OP._speed Medium + , HE.onClick $ HE.input_ $ F.Modify $ F.setInput proxies.speed Medium ] , HH.text $ " " <> show Medium ] @@ -175,10 +175,10 @@ renderSpeed state = , css "radio" , HP.type_ InputRadio , HP.checked $ speed.input == Fast - , HE.onClick $ HE.input_ $ F.Modify $ F.setInput OP._speed Fast + , HE.onClick $ HE.input_ $ F.Modify $ F.setInput proxies.speed Fast ] , HH.text $ " " <> show Fast ] ] where - speed = view (F._Field OP._speed) state.form + speed = view (F._Field proxies.speed) state.form diff --git a/example/real-world/Spec/GroupForm.purs b/example/real-world/Spec/GroupForm.purs index 096022b..8e22df6 100644 --- a/example/real-world/Spec/GroupForm.purs +++ b/example/real-world/Spec/GroupForm.purs @@ -5,16 +5,14 @@ import Prelude import Data.Maybe (Maybe(..)) import Data.Symbol (SProxy(..)) import Data.Validation.Semigroup (V) -import Example.RealWorld.Data.Group (Group(..), GroupForm, GroupFormRow, GroupId(..), _secretKey1, _secretKey2) +import Example.RealWorld.Data.Group (Group(..), GroupForm, GroupId(..), proxies) import Example.App.Validation as V import Formless as F import Formless.Validation.Semigroup (applyOnInputFields) import Record as Record -import Type.Row (RProxy(..)) groupFormSpec :: GroupForm F.FormSpec -groupFormSpec = - F.mkFormSpecFromRow $ RProxy :: RProxy (GroupFormRow F.InputType) +groupFormSpec = F.mkFormSpecFromProxy $ F.FormProxy :: F.FormProxy GroupForm groupFormSubmit :: ∀ m. Monad m => GroupForm F.OutputField -> m Group groupFormSubmit form = do @@ -34,9 +32,9 @@ groupFormValidate form = pure $ applyOnInputFields ( identity { name: V.validateNonEmpty , secretKey1: - \i -> V.validateNonEmpty i *> V.validateEqual (F.getInput _secretKey2 form) i + \i -> V.validateNonEmpty i *> V.validateEqual (F.getInput proxies.secretKey2 form) i , secretKey2: - \i -> V.validateNonEmpty i *> V.validateEqual (F.getInput _secretKey1 form) i + \i -> V.validateNonEmpty i *> V.validateEqual (F.getInput proxies.secretKey1 form) i , admin: V.validateMaybe , applications: V.validateNonEmptyArray , pixels: V.validateNonEmptyArray diff --git a/example/real-world/Spec/OptionsForm.purs b/example/real-world/Spec/OptionsForm.purs index 2c91cce..8f532f5 100644 --- a/example/real-world/Spec/OptionsForm.purs +++ b/example/real-world/Spec/OptionsForm.purs @@ -5,13 +5,12 @@ import Prelude import Data.Int as Int import Data.Maybe (Maybe(..)) import Example.App.Validation as V -import Example.RealWorld.Data.Options (Dollars(..), Metric(..), OptionsForm(..), OptionsRow, Speed(..), _metric) +import Example.RealWorld.Data.Options (Dollars(..), Metric(..), OptionsForm(..), Speed(..), proxies) import Formless as F import Formless.Validation.Semigroup (onInputField) -import Type.Row (RProxy(..)) optionsFormSpec :: OptionsForm F.FormSpec -optionsFormSpec = F.mkFormSpecFromRow $ RProxy :: RProxy (OptionsRow F.InputType) +optionsFormSpec = F.mkFormSpecFromProxy $ F.FormProxy :: F.FormProxy OptionsForm -- In the case the user has not toggled the options on, we'll provide them with -- valid default values @@ -41,7 +40,7 @@ optionsFormValidate (OptionsForm form) = OptionsForm , speed: pure `onInputField` form.speed } where - metric = F.getInput _metric (OptionsForm form) + metric = F.getInput proxies.metric (OptionsForm form) validateMetric m str | metric == Just m = pure <<< Dollars <$> V.validateInt str diff --git a/src/Formless.purs b/src/Formless.purs index 39e0a91..8172f3b 100644 --- a/src/Formless.purs +++ b/src/Formless.purs @@ -44,8 +44,8 @@ import Data.Symbol (SProxy(..)) import Data.Traversable (traverse, traverse_) import Formless.Class.Initial (class Initial, initial) import Formless.Internal as Internal -import Formless.Spec (ErrorType, FormSpec(..), InputField(..), InputFieldRow, InputType, OutputField(..), OutputType, _Error, _Field, _Input, _Output, _Result, _Touched, _input, _result, _touched) -import Formless.Spec.Transform (class MakeFormSpecFromRow, getInput, getResult, mkFormSpec, mkFormSpecFromRow, mkFormSpecFromRowBuilder, modifyInput, resetField, setInput, touchField, unwrapOutput) +import Formless.Spec (ErrorType, FormProxy(..), FormSpec(..), InputField(..), InputFieldRow, InputType, OutputField(..), OutputType, _Error, _Field, _Input, _Output, _Result, _Touched, _input, _result, _touched) +import Formless.Spec.Transform (class MakeFormSpecFromRow, class MakeSProxies, SProxies, getInput, getResult, makeSProxiesBuilder, mkFormSpec, mkFormSpecFromProxy, mkFormSpecFromRowBuilder, mkSProxies, modifyInput, resetField, setInput, touchField, unwrapOutput) import Halogen as H import Halogen.Component.ChildPath (ChildPath, injQuery, injSlot) import Halogen.HTML as HH diff --git a/src/Internal/Internal.purs b/src/Internal/Internal.purs index bc62aad..8dc5027 100644 --- a/src/Internal/Internal.purs +++ b/src/Internal/Internal.purs @@ -18,8 +18,7 @@ import Type.Data.RowList (RLProxy(..)) ----- -- Types --- | Never exposed to the user, but used to aid equality instances for --- | checking dirty states. +-- | Used to aid equality instances for checking dirty states. newtype Input e i o = Input i derive instance newtypeInput :: Newtype (Input e i o) _ derive newtype instance eqInput :: Eq i => Eq (Input e i o) diff --git a/src/Spec/Spec.purs b/src/Spec/Spec.purs index 1e72dc7..d57c5b8 100644 --- a/src/Spec/Spec.purs +++ b/src/Spec/Spec.purs @@ -14,6 +14,9 @@ import Data.Newtype (class Newtype) import Data.Symbol (class IsSymbol, SProxy(..)) import Prim.Row (class Cons) +-- | @monoidmusician +data FormProxy (form :: (Type -> Type -> Type -> Type) -> Type) = FormProxy + -- | The type that will be applied to the user's input row to -- | create the spec form that we'll compare against to measure -- | 'touched' states, etc. This is what the user is responsible diff --git a/src/Spec/Transform.purs b/src/Spec/Transform.purs index 971d6b9..197a883 100644 --- a/src/Spec/Transform.purs +++ b/src/Spec/Transform.purs @@ -9,11 +9,11 @@ import Data.Newtype (class Newtype, unwrap, wrap) import Data.Symbol (class IsSymbol, SProxy(..)) import Formless.Class.Initial (class Initial, initial) import Formless.Internal as Internal -import Formless.Spec (FormSpec(..), InputField, OutputField, _Input, _Result, _Touched) +import Formless.Spec (FormProxy, FormSpec(..), InputField, OutputField, _Input, _Result, _Touched) import Prim.Row as Row import Prim.RowList as RL import Record.Builder as Builder -import Type.Row (RLProxy(..), RProxy) +import Type.Row (RLProxy(..), RProxy(..)) getInput :: ∀ sym form t0 fields e i o @@ -149,19 +149,22 @@ mkFormSpec = wrap <<< Internal.wrapRecord -- | , age :: f String String Int -- | ) -- | --- | -- To retrieve input types only, use the Input type synonym -- | formSpec :: Form FormSpec --- | formSpec = mkFormSpecFromRow (RProxy :: RProxy (MyRow Input)) +-- | formSpec = mkFormSpecFromProxy (FormProxy :: FormProxy Form) -- | ``` -mkFormSpecFromRow - :: ∀ row xs row' form +mkFormSpecFromProxy + :: ∀ row xs row' form' form . RL.RowToList row xs => MakeFormSpecFromRow xs row row' - => Newtype (form FormSpec) (Record row') - => RProxy row - -> form FormSpec -mkFormSpecFromRow r = wrap $ Internal.fromScratch builder - where builder = mkFormSpecFromRowBuilder (RLProxy :: RLProxy xs) r + => Newtype (form Internal.Input) (Record row) + => Newtype (form' FormSpec) (Record row') + => FormProxy form + -> form' FormSpec +mkFormSpecFromProxy _ = wrap $ Internal.fromScratch builder + where + builder = mkFormSpecFromRowBuilder + (RLProxy :: RLProxy xs) + (RProxy :: RProxy row) -- | The class that provides the Builder implementation to efficiently -- | transform a row into a proper FormSpec by wrapping it in newtypes and @@ -175,11 +178,11 @@ instance mkFormSpecFromRowNil :: MakeFormSpecFromRow RL.Nil row () where instance mkFormSpecFromRowCons :: ( IsSymbol name , Initial i - , Row.Cons name i trash row + , Row.Cons name (Internal.Input e i o) trash row , MakeFormSpecFromRow tail row from , Internal.Row1Cons name (FormSpec e i o) from to ) - => MakeFormSpecFromRow (RL.Cons name i tail) row to where + => MakeFormSpecFromRow (RL.Cons name (Internal.Input e i o) tail) row to where mkFormSpecFromRowBuilder _ r = first <<< rest where @@ -187,3 +190,62 @@ instance mkFormSpecFromRowCons val = FormSpec initial rest = mkFormSpecFromRowBuilder (RLProxy :: RLProxy tail) r first = Builder.insert _name val + + +-- | A type to collect constraints necessary to apply to prove that a record of +-- | SProxies is compatible with your form type. +type SProxies form = + ∀ row xs row' + . RL.RowToList row xs + => MakeSProxies xs row' + => Newtype (form FormSpec) (Record row) + => Record row' + +-- | A helper function to produce a record of SProxies given a form spec, to save +-- | you the boilerplate of writing them all out. +-- | +-- | ```purescript +-- | 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) _ +-- | +-- | proxies :: Proxies Form +-- | proxies = mkSProxies (FormProxy :: FormProxy Form) +-- | +-- | -- You can now access all your proxies from the record with dot syntax +-- | _name :: SProxy "name" +-- | _name = proxies.name +-- | ``` +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) + +-- | The class used to build up a new record of symbol proxies from an +-- | input row list. +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)