{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DefaultSignatures #-} {-# LANGUAGE UndecidableInstances #-} module Web.Hyperbole.HyperView.Forms ( FromForm (..) , FromFormF (..) , GenFields (..) , fieldNames , FieldName (..) , FormFields (..) , Field , InputType (..) , Input (..) , field , label , input , checkbox , Radio (..) , radioGroup , radio , select , form , textarea , submit , formData , FormOptions (..) , Validated (..) , isInvalid , invalidText , validate , Identity -- * Re-exports , FE.FromFormKey , Generic , GFieldsGen (..) , GenField (..) , Form (..) ) where import Data.Bifunctor (first) import Data.Functor.Identity (Identity (..)) import Data.Kind (Type) import Data.Maybe (fromMaybe) import Data.String (IsString (..)) import Data.String.Conversions (cs) import Data.Text (Text, pack) import Effectful import GHC.Generics import Text.Casing (kebab) import Web.Atomic.Types hiding (Selector) import Web.FormUrlEncoded (Form (..), FormOptions (..)) import Web.FormUrlEncoded qualified as FE import Web.Hyperbole.Data.Param import Web.Hyperbole.Effect.Hyperbole import Web.Hyperbole.Effect.Request import Web.Hyperbole.Effect.Response (parseError) import Web.Hyperbole.HyperView.Event (onSubmit) import Web.Hyperbole.HyperView.Input (Option (..), checked) import Web.Hyperbole.HyperView.Types import Web.Hyperbole.HyperView.ViewAction import Web.Hyperbole.View ------------------------------------------------------------------------------ -- FORM PARSING ------------------------------------------------------------------------------ {- | Simple types that be decoded from form data @ #EMBED Example/Page/FormSimple.hs data ContactForm @ -} class FromForm (form :: Type) where fromForm :: FE.Form -> Either String form default fromForm :: (Generic form, GFormParse (Rep form)) => FE.Form -> Either String form fromForm f = to <$> gFormParse f {- | A Higher-Kinded type that can be parsed from a 'Web.FormUrlEncoded.Form' @ #EMBED Example/Page/FormValidation.hs data UserForm @ -} class FromFormF (f :: (Type -> Type) -> Type) where fromFormF :: FE.Form -> Either String (f Identity) default fromFormF :: (Generic (f Identity), GFormParse (Rep (f Identity))) => FE.Form -> Either String (f Identity) fromFormF f = to <$> gFormParse f -- Any FromFormF can be parsed using fromForm @(form Identity) -- we can't make it an instance because it is an orphan instance instance (FromFormF form) => FromForm (form Identity) where fromForm = fromFormF -- | Parse a full type from a submitted form body formData :: forall form es. (FromForm form, Hyperbole :> es) => Eff es form formData = do f <- formBody let ef = fromForm @form f :: Either String form either parseError pure ef ------------------------------------------------------------------------------ -- GEN FIELDS: Generate a type from selector names ------------------------------------------------------------------------------ {- | Generate a Higher Kinded record with all selectors filled with default values. See 'GenField' @ #EMBED Example/Page/FormValidation.hs data UserForm @ @ #EMBED Example/Page/Contacts.hs newContactForm @ -} class GenFields f (form :: (Type -> Type) -> Type) where genFields :: form f default genFields :: (Generic (form f), GFieldsGen (Rep (form f))) => form f genFields = to gFieldsGen {- | Generate FieldNames for a form #EXAMPLE /forms > #EMBED Example/Page/Todos/Todo.hs data TodoForm > > #EMBED Example/Page/Todos/Todo.hs todoForm -} fieldNames :: forall form. (GenFields FieldName form) => form FieldName fieldNames = genFields -- Given a selector, generate the type class GenField a where genField :: String -> a instance GenField (FieldName a) where genField s = FieldName $ pack s instance GenField (Validated a) where genField = const NotInvalid instance GenField (Maybe a) where genField _ = Nothing ------------------------------------------------------------------------------ -- FORM VIEWS ------------------------------------------------------------------------------ -- | Context that allows form fields data FormFields id = FormFields id {- | Type-safe \