-- Hoogle documentation, generated by Haddock -- See Hoogle, http://www.haskell.org/hoogle/ -- | Interactive HTML apps using type-safe serverside Haskell -- -- Interactive HTML applications using type-safe serverside Haskell. -- Inspired by HTMX, Elm, and Phoenix LiveView @package hyperbole @version 0.3.6 module Web.Hyperbole.Embed -- | Default CSS to remove unintuitive default styles. This or -- cssResetLink is required. -- --
-- import Data.String.Interpolate (i)
--
-- toDocument :: ByteString -> ByteString
-- toDocument cnt =
-- [i|<html>
-- <head>
-- <style type="text/css">#{cssResetEmbed}</style>
-- </head>
-- <body>#{cnt}</body>
-- </html>|]
--
cssResetEmbed :: ByteString
-- | Alternatively, the reset is available on a CDN
--
--
-- import Data.String.Interpolate (i)
--
-- toDocument :: ByteString -> ByteString
-- toDocument cnt =
-- [i|<html>
-- <head>
-- <link rel="stylesheet" href="#{cssResetLink}">
-- </head>
-- <body>#{cnt}</body>
-- </html>|]
--
cssResetLink :: Text
scriptEmbed :: ByteString
module Web.Hyperbole.Route
-- | Derive this class to use a sum type as a route. Constructors and
-- Selectors map intuitively to url patterns
--
-- -- data AppRoute -- = HomePage -- | Users -- | User Int -- deriving (Generic, Route) -- -- / -> HomePage -- /users/ -> Users -- /user/100 -> User 100 --class Route a -- | The default route to use if attempting to match on empty segments defRoute :: Route a => a -- | Map a route to segments routePath :: Route a => a -> [Segment] -- | Try to match segments to a route matchRoute :: Route a => [Segment] -> Maybe a -- | Try to match segments to a route matchRoute :: (Route a, Generic a, GenRoute (Rep a)) => [Segment] -> Maybe a -- | Map a route to segments routePath :: (Route a, Generic a, Eq a, GenRoute (Rep a)) => a -> [Segment] -- | The default route to use if attempting to match on empty segments defRoute :: (Route a, Generic a, GenRoute (Rep a)) => a -- | Try to match a route, use defRoute if it's empty findRoute :: Route a => [Segment] -> Maybe a pathUrl :: [Segment] -> Url -- | Convert a Route to a Url -- --
-- >>> routeUrl (User 100) -- /user/100 --routeUrl :: Route a => a -> Url -- | Automatically derive Route class GenRoute f genRoute :: GenRoute f => [Text] -> Maybe (f p) genPaths :: GenRoute f => f p -> [Text] genFirst :: GenRoute f => f p genRouteRead :: Read x => [Text] -> Maybe (K1 R x a) data () => Url instance Web.Hyperbole.Route.Route sub => Web.Hyperbole.Route.GenRoute (GHC.Generics.K1 GHC.Generics.R sub) instance Web.Hyperbole.Route.Route Data.Text.Internal.Text instance Web.Hyperbole.Route.Route GHC.Base.String instance Web.Hyperbole.Route.Route GHC.Num.Integer.Integer instance Web.Hyperbole.Route.Route GHC.Types.Int instance Web.Hyperbole.Route.Route a => Web.Hyperbole.Route.Route (GHC.Maybe.Maybe a) instance forall k (f :: k -> *) (c :: GHC.Generics.Meta). Web.Hyperbole.Route.GenRoute f => Web.Hyperbole.Route.GenRoute (GHC.Generics.M1 GHC.Generics.D c f) instance forall k (c :: GHC.Generics.Meta) (f :: k -> *). (GHC.Generics.Constructor c, Web.Hyperbole.Route.GenRoute f) => Web.Hyperbole.Route.GenRoute (GHC.Generics.M1 GHC.Generics.C c f) instance Web.Hyperbole.Route.GenRoute GHC.Generics.U1 instance forall k (f :: k -> *) (c :: GHC.Generics.Meta). Web.Hyperbole.Route.GenRoute f => Web.Hyperbole.Route.GenRoute (GHC.Generics.M1 GHC.Generics.S c f) instance forall k (a :: k -> *) (b :: k -> *). (Web.Hyperbole.Route.GenRoute a, Web.Hyperbole.Route.GenRoute b) => Web.Hyperbole.Route.GenRoute (a GHC.Generics.:+: b) instance forall k (a :: k -> *) (b :: k -> *). (Web.Hyperbole.Route.GenRoute a, Web.Hyperbole.Route.GenRoute b) => Web.Hyperbole.Route.GenRoute (a GHC.Generics.:*: b) module Web.Hyperbole.HyperView -- | HyperViews are interactive subsections of a Page -- -- Create an instance with a unique view id type and a sum type -- describing the actions the HyperView supports. The View Id can contain -- context (a database id, for example) -- --
-- data Message = Message Int -- deriving (Generic, Param) -- -- data MessageAction -- = Louder Text -- | ClearMessage -- deriving (Generic, Param) -- -- instance HyperView Message where -- type Action Message = MessageAction --class (Param id, Param (Action id)) => HyperView id where { type Action id :: Type; } -- | Embed HyperViews into the page, or nest them into other views -- --
-- myPage :: (Hyperbole :> es) => Page es Response -- myPage = do -- handle messages -- load $ do -- pure $ do -- el_ "My Page" -- hyper (Message 1) $ messageView "Hello World" -- hyper (Message 2) $ do -- messageView "Another Message" -- hyper OtherView otherView ---- -- Views can only trigger actions that match their HyperView -- --
-- messageView :: Text -> View Message () -- messageView m = do -- el_ (text m) -- button (Louder m) Louder -- -- otherView :: View OtherView () -- otherView = do -- -- Type Error! -- button (Louder "Hi") id Louder --hyper :: forall id ctx. HyperView id => id -> View id () -> View ctx () -- | <button> HTML tag which sends the action when pressed -- --
-- button SomeAction (border 1) "Click Me" --button :: HyperView id => Action id -> Mod -> View id () -> View id () -- | Send the action after N milliseconds. Can be used to implement lazy -- loading or polling -- --
-- pollMessageView :: Text -> View Message () -- pollMessageView m = do -- onLoad LoadMessage 1000 $ do -- el bold "Current Message. Reloading in 1s" -- el_ (text m) --onLoad :: HyperView id => Action id -> DelayMs -> View id () -> View id () type DelayMs = Int -- | Give visual feedback when an action is in-flight. -- --
-- myView = do -- onRequest loadingIndicator $ do -- el_ "Loaded" -- where -- loadingIndicator = el_ "Loading..." --onRequest :: View id () -> View id () -> View id () -- | Internal dataTarget :: Param a => a -> Mod -- | Trigger actions for another view. They will update the view specified -- --
-- otherView :: View OtherView () -- otherView = do -- el_ "This is not a message view" -- button OtherAction id "Do Something" -- -- target (Message 2) $ do -- el_ "Now we can trigger a MessageAction which will update our Message HyperView, not this one" -- button ClearMessage id "Clear Message #2" --target :: HyperView id => id -> View id () -> View a () -- | Type-safe dropdown. Sends (opt -> Action id) when selected. The -- selection predicate (opt -> Bool) controls which option is -- selected. See Example.Contacts -- --
-- data ContactsAction -- = Reload (Maybe Filter) -- | Delete Int -- deriving (Generic, Param) -- -- allContactsView :: Maybe Filter -> View Contacts () -- allContactsView fil = do -- row (gap 10) $ do -- el (pad 10) "Filter: " -- dropdown Reload (== fil) id $ do -- option Nothing "" -- option (Just Active) "Active!" -- option (Just Inactive) Inactive -- ... --dropdown :: HyperView id => (opt -> Action id) -> (opt -> Bool) -> Mod -> View (Option opt id (Action id)) () -> View id () -- | An option for a dropdown. First argument is passed to (opt -- -> Action id) in the dropdown, and to the selected predicate option :: (HyperView id, Eq opt) => opt -> View (Option opt id (Action id)) () -> View (Option opt id (Action id)) () -- | sets selected = true if the dropdown predicate returns True selected :: Bool -> Mod -- | The view context for an option data Option opt id action Option :: (opt -> action) -> (opt -> Bool) -> Option opt id action [$sel:toAction:Option] :: Option opt id action -> opt -> action [$sel:selected:Option] :: Option opt id action -> opt -> Bool -- | Types that can be serialized. HyperView requires this for both -- its view id and action -- --
-- data Message = Message Int -- deriving (Generic, Param) --class Param a toParam :: Param a => a -> Text toParam :: (Param a, Generic a, GParam (Rep a)) => a -> Text parseParam :: Param a => Text -> Maybe a parseParam :: (Param a, Generic a, GParam (Rep a)) => Text -> Maybe a class GParam f gToParam :: GParam f => f p -> Text gParseParam :: GParam f => Text -> Maybe (f p) breakSegment :: Text -> (Text, Text) toSegment :: String -> Text -- | A hyperlink to another route -- --
-- >>> route (User 100) id "View User" -- <a href="/user/100">View User</a> --route :: Route a => a -> Mod -> View c () -> View c () instance Web.Hyperbole.HyperView.Param a => Web.Hyperbole.HyperView.GParam (GHC.Generics.K1 GHC.Generics.R a) instance Web.Hyperbole.HyperView.Param a => Web.Hyperbole.HyperView.Param (GHC.Maybe.Maybe a) instance Web.Hyperbole.HyperView.Param GHC.Num.Integer.Integer instance Web.Hyperbole.HyperView.Param GHC.Types.Float instance Web.Hyperbole.HyperView.Param GHC.Types.Int instance Web.Hyperbole.HyperView.Param () instance Web.Hyperbole.HyperView.Param Data.Text.Internal.Text instance forall k (f :: k -> *) (g :: k -> *). (Web.Hyperbole.HyperView.GParam f, Web.Hyperbole.HyperView.GParam g) => Web.Hyperbole.HyperView.GParam (f GHC.Generics.:*: g) instance forall k (f :: k -> *) (g :: k -> *). (Web.Hyperbole.HyperView.GParam f, Web.Hyperbole.HyperView.GParam g) => Web.Hyperbole.HyperView.GParam (f GHC.Generics.:+: g) instance forall k (d :: GHC.Generics.Meta) (f :: k -> *). (GHC.Generics.Datatype d, Web.Hyperbole.HyperView.GParam f) => Web.Hyperbole.HyperView.GParam (GHC.Generics.M1 GHC.Generics.D d f) instance forall k (c :: GHC.Generics.Meta) (f :: k -> *). (GHC.Generics.Constructor c, Web.Hyperbole.HyperView.GParam f) => Web.Hyperbole.HyperView.GParam (GHC.Generics.M1 GHC.Generics.C c f) instance Web.Hyperbole.HyperView.GParam GHC.Generics.U1 instance forall k (f :: k -> *) (s :: GHC.Generics.Meta). Web.Hyperbole.HyperView.GParam f => Web.Hyperbole.HyperView.GParam (GHC.Generics.M1 GHC.Generics.S s f) instance Web.Hyperbole.HyperView.GParam (GHC.Generics.K1 GHC.Generics.R Data.Text.Internal.Text) instance Web.Hyperbole.HyperView.GParam (GHC.Generics.K1 GHC.Generics.R GHC.Base.String) module Web.Hyperbole.Session newtype Session Session :: Map Text Text -> Session -- | Set the session key to value sessionSet :: ToHttpApiData a => Text -> a -> Session -> Session sessionDel :: Text -> Session -> Session sessionLookup :: FromHttpApiData a => Text -> Session -> Maybe a sessionEmpty :: Session -- | Render a session as a url-encoded query string sessionRender :: Session -> ByteString -- | Parse a session as a url-encoded query string sessionParse :: ByteString -> Session sessionFromCookies :: [(ByteString, ByteString)] -> Session sessionSetCookie :: Session -> ByteString instance GHC.Show.Show Web.Hyperbole.Session.Session module Web.Hyperbole.Effect newtype Host Host :: ByteString -> Host [$sel:text:Host] :: Host -> ByteString data Request Request :: Host -> [Segment] -> Query -> ByteString -> Method -> [(ByteString, ByteString)] -> Request [$sel:host:Request] :: Request -> Host [$sel:path:Request] :: Request -> [Segment] [$sel:query:Request] :: Request -> Query [$sel:body:Request] :: Request -> ByteString [$sel:method:Request] :: Request -> Method [$sel:cookies:Request] :: Request -> [(ByteString, ByteString)] -- | Valid responses for a Hyperbole effect. Use notFound, -- etc instead. Reminds you to use load in your Page -- --
-- myPage :: (Hyperbole :> es) => Page es Response -- myPage = do -- -- compiler error: () does not equal Response -- pure () --data Response Response :: View () () -> Response NotFound :: Response Redirect :: Url -> Response Err :: ResponseError -> Response Empty :: Response data ResponseError ErrParse :: Text -> ResponseError ErrParam :: Text -> ResponseError ErrOther :: Text -> ResponseError ErrNotHandled :: Event Text Text -> ResponseError ErrAuth :: ResponseError -- | Hyperbole applications are divided into Pages. Each Page must -- load the whole page , and handle each type of -- HyperView -- --
-- myPage :: (Hyperbole :> es) => Page es Response -- myPage = do -- handle messages -- load pageView -- -- pageView = do -- el_ "My Page" -- hyper (Message 1) $ messageView "Starting Message" --newtype Page es a Page :: Eff es a -> Page es a -- | An action, with its corresponding id data Event id act Event :: id -> act -> Event id act [$sel:viewId:Event] :: Event id act -> id [$sel:action:Event] :: Event id act -> act -- | Low level effect mapping request/response to either HTTP or WebSockets data Server :: Effect [LoadRequest] :: Server m Request [SendResponse] :: Session -> Response -> Server m () -- | In any load or handle, you can use this Effect to get -- extra request information or control the response manually. -- -- For most Pages, you won't need to use this effect directly. Use -- custom Routes for request info, and return Views to -- respond data Hyperbole :: Effect [GetRequest] :: Hyperbole m Request [RespondEarly] :: Response -> Hyperbole m a [SetSession] :: ToHttpApiData a => Text -> a -> Hyperbole m () [DelSession] :: Text -> Hyperbole m () [GetSession] :: FromHttpApiData a => Text -> Hyperbole m (Maybe a) data HyperState HyperState :: Request -> Session -> HyperState [$sel:request:HyperState] :: HyperState -> Request [$sel:session:HyperState] :: HyperState -> Session -- | Run the Hyperbole effect to Server runHyperbole :: Server :> es => Eff (Hyperbole : es) Response -> Eff es Response -- | Return all information about the Request request :: Hyperbole :> es => Eff es Request -- | Return the request path -- --
-- >>> reqPath -- ["users", "100"] --reqPath :: Hyperbole :> es => Eff es [Segment] -- | Return the request body as a Web.FormUrlEncoded.Form -- -- Prefer using Type-Safe Forms when possible formData :: Hyperbole :> es => Eff es Form getEvent :: (HyperView id, Hyperbole :> es) => Eff es (Maybe (Event id (Action id))) parseEvent :: HyperView id => Query -> Maybe (Event id (Action id)) lookupEvent :: Query -> Maybe (Event Text Text) -- | Lookup a session variable by keyword -- --
-- load $ do -- tok <- session "token" -- ... --session :: (Hyperbole :> es, FromHttpApiData a) => Text -> Eff es (Maybe a) -- | Set a session variable by keyword -- --
-- load $ do -- t <- reqParam "token" -- setSession "token" t -- ... --setSession :: (Hyperbole :> es, ToHttpApiData a) => Text -> a -> Eff es () -- | Clear the user's session clearSession :: Hyperbole :> es => Text -> Eff es () -- | Return the entire Query -- --
-- myPage :: Page es Response -- myPage = do -- load $ do -- q <- reqParams -- case lookupParam "token" q of -- Nothing -> pure $ errorView "Missing Token in Query String" -- Just t -> do -- sideEffectUsingToken token -- pure myPageView --reqParams :: Hyperbole :> es => Eff es Query -- | Lookup the query param in the Query lookupParam :: ByteString -> Query -> Maybe Text -- | Require a given parameter from the Query arguments -- --
-- myPage :: Page es Response -- myPage = do -- load $ do -- token <- reqParam "token" -- sideEffectUsingToken token -- pure myPageView --reqParam :: (Hyperbole :> es, FromHttpApiData a) => Text -> Eff es a -- | Respond immediately with 404 Not Found -- --
-- userLoad :: (Hyperbole :> es, Users :> es) => UserId -> Eff es User -- userLoad uid = do -- mu <- send (LoadUser uid) -- maybe notFound pure mu -- -- myPage :: (Hyperbole :> es, Users :> es) => Eff es View -- myPage = do -- load $ do -- u <- userLoad 100 -- -- skipped if user = Nothing -- pure $ userView u --notFound :: Hyperbole :> es => Eff es a -- | Respond immediately with a parse error parseError :: Hyperbole :> es => Text -> Eff es a -- | Redirect immediately to the Url redirect :: Hyperbole :> es => Url -> Eff es a -- | Respond with the given view, and stop execution respondEarly :: (Hyperbole :> es, HyperView id) => id -> View id () -> Eff es () -- | Manually set the response to the given view. Normally you return a -- View from load or handle instead of using this view :: Hyperbole :> es => View () () -> Eff es Response -- | The load handler is run when the page is first loaded. Run any side -- effects needed, then return a view of the full page -- --
-- myPage :: (Hyperbole :> es) => UserId -> Page es Response -- myPage userId = do -- load $ do -- user <- loadUserFromDatabase userId -- pure $ userPageView user --load :: Hyperbole :> es => Eff es (View () ()) -> Page es Response -- | A handler is run when an action for that HyperView is -- triggered. Run any side effects needed, then return a view of the -- corresponding type -- --
-- myPage :: (Hyperbole :> es) => Page es Response -- myPage = do -- handle messages -- load pageView -- -- messages :: (Hyperbole :> es, MessageDatabase) => Message -> MessageAction -> Eff es (View Message ()) -- messages (Message mid) ClearMessage = do -- deleteMessageSideEffect mid -- pure $ messageView "" -- -- messages (Message mid) (Louder m) = do -- let new = m <> "!" -- saveMessageSideEffect mid new -- pure $ messageView new --handle :: forall id es. (Hyperbole :> es, HyperView id) => (id -> Action id -> Eff es (View id ())) -> Page es () -- | Run a Page in Hyperbole page :: Hyperbole :> es => Page es Response -> Eff es Response instance GHC.Show.Show Web.Hyperbole.Effect.Host instance GHC.Show.Show Web.Hyperbole.Effect.Request instance GHC.Base.Functor (Web.Hyperbole.Effect.Page es) instance GHC.Base.Monad (Web.Hyperbole.Effect.Page es) instance GHC.Base.Applicative (Web.Hyperbole.Effect.Page es) instance GHC.Show.Show Web.Hyperbole.Effect.ResponseError instance (GHC.Show.Show act, GHC.Show.Show id) => GHC.Show.Show (Web.Hyperbole.Effect.Event id act) module Web.Hyperbole.Forms -- | The only time we can use Fields is inside a form data FormFields id FormFields :: id -> Validation -> FormFields id -- | Choose one for inputs to give the browser autocomplete hints data InputType NewPassword :: InputType CurrentPassword :: InputType Username :: InputType Email :: InputType Number :: InputType TextInput :: InputType Name :: InputType OneTimeCode :: InputType Organization :: InputType StreetAddress :: InputType Country :: InputType CountryName :: InputType PostalCode :: InputType Search :: InputType data Label a data Invalid a data Input id a Input :: Text -> Validation -> Input id a -- | Display a FormField -- --
-- data Age = Age Int deriving (Generic, FormField) -- -- myForm = do -- form SignUp mempty id $ do -- field @Age id id $ do -- label Age -- input Number (value "0") --field :: forall a id. FormField a => Mod -> Mod -> View (Input id a) () -> View (FormFields id) () -- | label for a field label :: Text -> View (Input id a) () -- | input for a field input :: InputType -> Mod -> View (Input id a) () -- | Type-safe <form>. Calls (Action id) on submit -- --
-- userForm :: Validation -> View FormView () -- userForm v = do -- form Signup v id $ do -- el Style.h1 "Sign Up" -- -- field @User id Style.invalid $ do -- label "Username" -- input Username (placeholder "username") -- el_ invalidText -- -- field @Age id Style.invalid $ do -- label "Age" -- input Number (placeholder "age" . value "0") -- el_ invalidText -- -- submit (border 1) "Submit" --form :: forall id. HyperView id => Action id -> Validation -> Mod -> View (FormFields id) () -> View id () placeholder :: Text -> Mod -- | Button that submits the form. Use button to specify -- actions other than submit submit :: Mod -> View (FormFields id) () -> View (FormFields id) () parseForm :: forall form es. (Form form, Hyperbole :> es) => Eff es (form Identity) -- | Parse a FormField from the request -- --
-- formAction :: (Hyperbole :> es, UserDB :> es) => FormView -> FormAction -> Eff es (View FormView ()) -- formAction _ SignUp = do -- a <- formField @Age -- u <- formField @User -- saveUserToDB u a -- pure $ el_ "Saved!" --formField :: forall a es. (FormField a, Hyperbole :> es) => Eff es a class Form (form :: (Type -> Type) -> Type) formLabels :: Form form => form Label formLabels :: (Form form, Generic (form Label), GForm (Rep (form Label))) => form Label formInvalid :: Form form => form Invalid formInvalid :: (Form form, Generic (form Invalid), GForm (Rep (form Invalid))) => form Invalid fromForm :: Form form => Form -> Either Text (form Identity) fromForm :: (Form form, Generic (form Identity), GFromForm (form Identity) (Rep (form Identity))) => Form -> Either Text (form Identity) -- | Default encoding FormOptions. -- --
-- FormOptions
-- { fieldLabelModifier = id
-- }
--
defaultFormOptions :: FormOptions
-- | Generic-based deriving options for ToForm and
-- FromForm.
--
-- A common use case for non-default FormOptions is to strip a
-- prefix off of field labels:
--
--
-- data Project = Project
-- { projectName :: String
-- , projectSize :: Int
-- } deriving (Generic, Show)
--
-- myOptions :: FormOptions
-- myOptions = FormOptions
-- { fieldLabelModifier = map toLower . drop (length "project") }
--
-- instance ToForm Project where
-- toForm = genericToForm myOptions
--
-- instance FromForm Project where
-- fromForm = genericFromForm myOptions
--
--
--
-- >>> urlEncodeAsFormStable Project { projectName = "http-api-data", projectSize = 172 }
-- "name=http-api-data&size=172"
--
-- >>> urlDecodeAsForm "name=http-api-data&size=172" :: Either Text Project
-- Right (Project {projectName = "http-api-data", projectSize = 172})
--
data () => FormOptions
FormOptions :: (String -> String) -> FormOptions
-- | Function applied to field labels. Handy for removing common record
-- prefixes for example.
[fieldLabelModifier] :: FormOptions -> String -> String
-- | A Generic-based implementation of fromForm. This is used
-- as a default implementation in FromForm.
--
-- Note that this only works for records (i.e. product data types with
-- named fields):
--
--
-- data Person = Person
-- { name :: String
-- , age :: Int
-- } deriving (Generic)
--
--
-- In this implementation each field's value gets decoded using
-- parseQueryParam. Two field types are exceptions:
--
--
-- data Post = Post
-- { title :: String
-- , subtitle :: Maybe String
-- , comments :: [String]
-- } deriving (Generic, Show)
--
-- instance FromForm Post
--
--
--
-- >>> urlDecodeAsForm "comments=Nice%20post%21&comments=%2B1&title=Test" :: Either Text Post
-- Right (Post {title = "Test", subtitle = Nothing, comments = ["Nice post!","+1"]})
--
genericFromForm :: (Generic a, GFromForm a (Rep a)) => FormOptions -> Form -> Either Text a
-- | Validation results for a form
--
-- -- validateUser :: User -> Age -> Validation -- validateUser (User u) (Age a) = -- validation -- [ validate @Age (a < 20) "User must be at least 20 years old" -- , validate @User (T.elem ' ' u) "Username must not contain spaces" -- , validate @User (T.length u < 4) "Username must be at least 4 chars" -- ] -- -- formAction :: (Hyperbole :> es, UserDB :> es) => FormView -> FormAction -> Eff es (View FormView ()) -- formAction _ SignUp = do -- a <- formField @Age -- u <- formField @User -- -- case validateUser u a of -- Validation [] -> successView -- errs -> userForm v ---- -- @ newtype Validation Validation :: [(Text, Text)] -> Validation -- | Form Fields are identified by a type -- --
-- data User = User Text deriving (Generic, FormField) -- data Age = Age Int deriving (Generic, FormField) --class FormField a inputName :: FormField a => Text inputName :: (FormField a, Generic a, GDataName (Rep a)) => Text fieldParse :: FormField a => Text -> Form -> Either Text a fieldParse :: (FormField a, Generic a, GFieldParse (Rep a)) => Text -> Form -> Either Text a lookupInvalid :: forall a. FormField a => Validation -> Maybe Text invalidStyle :: forall a. FormField a => Mod -> Validation -> Mod -- | Display any validation error for the FormField from the -- Validation passed to form -- --
-- field @User id Style.invalid $ do -- label "Username" -- input Username (placeholder "username") -- el_ invalidText --invalidText :: forall a id. FormField a => View (Input id a) () -- | specify a check for a Validation validate :: forall a. FormField a => Bool -> Text -> Maybe (Text, Text) -- | Create a Validation from list of validators validation :: [Maybe (Text, Text)] -> Validation -- | Parse value from HTTP API data. -- -- WARNING: Do not derive this using DeriveAnyClass as -- the generated instance will loop indefinitely. class () => FromHttpApiData a -- | Representable types of kind *. This class is derivable in GHC -- with the DeriveGeneric flag on. -- -- A Generic instance must satisfy the following laws: -- --
-- from . to ≡ id -- to . from ≡ id --class () => Generic a instance GHC.Show.Show Web.Hyperbole.Forms.InputType instance GHC.Base.Monoid Web.Hyperbole.Forms.Validation instance GHC.Base.Semigroup Web.Hyperbole.Forms.Validation instance forall k (f :: k -> *) (d :: GHC.Generics.Meta). Web.Hyperbole.Forms.GFieldParse f => Web.Hyperbole.Forms.GFieldParse (GHC.Generics.M1 GHC.Generics.D d f) instance forall k (f :: k -> *) (c :: GHC.Generics.Meta). Web.Hyperbole.Forms.GFieldParse f => Web.Hyperbole.Forms.GFieldParse (GHC.Generics.M1 GHC.Generics.C c f) instance forall k (f :: k -> *) (s :: GHC.Generics.Meta). Web.Hyperbole.Forms.GFieldParse f => Web.Hyperbole.Forms.GFieldParse (GHC.Generics.M1 GHC.Generics.S s f) instance Web.Internal.HttpApiData.FromHttpApiData a => Web.Hyperbole.Forms.GFieldParse (GHC.Generics.K1 GHC.Generics.R a) instance forall k (d :: GHC.Generics.Meta) (c :: GHC.Generics.Meta) (f :: k -> *). GHC.Generics.Datatype d => Web.Hyperbole.Forms.GDataName (GHC.Generics.M1 GHC.Generics.D d (GHC.Generics.M1 GHC.Generics.C c f)) instance Web.Hyperbole.Forms.GForm GHC.Generics.U1 instance forall k (f :: k -> *) (g :: k -> *). (Web.Hyperbole.Forms.GForm f, Web.Hyperbole.Forms.GForm g) => Web.Hyperbole.Forms.GForm (f GHC.Generics.:*: g) instance forall k (f :: k -> *) (d :: GHC.Generics.Meta). Web.Hyperbole.Forms.GForm f => Web.Hyperbole.Forms.GForm (GHC.Generics.M1 GHC.Generics.D d f) instance forall k (f :: k -> *) (c :: GHC.Generics.Meta). Web.Hyperbole.Forms.GForm f => Web.Hyperbole.Forms.GForm (GHC.Generics.M1 GHC.Generics.C c f) instance GHC.Generics.Selector s => Web.Hyperbole.Forms.GForm (GHC.Generics.M1 GHC.Generics.S s (GHC.Generics.K1 GHC.Generics.R Data.Text.Internal.Text)) instance Web.Hyperbole.Forms.GForm (GHC.Generics.M1 GHC.Generics.S s (GHC.Generics.K1 GHC.Generics.R (GHC.Maybe.Maybe Data.Text.Internal.Text))) instance Web.Hyperbole.HyperView.Param id => Web.Hyperbole.HyperView.Param (Web.Hyperbole.Forms.FormFields id) instance (Web.Hyperbole.HyperView.HyperView id, Web.Hyperbole.HyperView.Param id) => Web.Hyperbole.HyperView.HyperView (Web.Hyperbole.Forms.FormFields id) module Web.Hyperbole.Application waiApp :: (ByteString -> ByteString) -> Eff '[Hyperbole, Server, IOE] Response -> Application -- | Upgrade a websockets ServerApp to a wai -- Application. Uses the given backup Application to handle -- Requests that are not WebSocket requests. -- --
-- websocketsOr opts ws_app backup_app = \req respond -> -- case websocketsApp opts ws_app req of -- Nothing -> backup_app req send_response -- Just res -> respond res ---- -- For example, below is an Application that sends "Hello, -- client!" to each connected client. -- --
-- app :: Application
-- app = websocketsOr defaultConnectionOptions wsApp backupApp
-- where
-- wsApp :: ServerApp
-- wsApp pending_conn = do
-- conn <- acceptRequest pending_conn
-- sendTextData conn ("Hello, client!" :: Text)
--
-- backupApp :: Application
-- backupApp _ respond = respond $ responseLBS status400 [] "Not a WebSocket request"
--
websocketsOr :: ConnectionOptions -> ServerApp -> Application -> Application
-- | The default connection options:
--
-- -- main = do -- run 3000 $ do -- liveApp (basicDocument "Example") $ do -- page mainPage --liveApp :: (ByteString -> ByteString) -> Eff '[Hyperbole, Server, IOE] Response -> Application socketApp :: IOE :> es => Eff (Hyperbole : (Server : es)) Response -> PendingConnection -> Eff es () runServerSockets :: IOE :> es => Connection -> Eff (Server : es) Response -> Eff es Response runServerWai :: IOE :> es => (ByteString -> ByteString) -> Request -> (Response -> IO ResponseReceived) -> Eff (Server : es) a -> Eff es (Maybe ResponseReceived) -- | wrap HTML fragments in a simple document with a custom title and -- include required embeds -- --
-- liveApp (basicDocument "App Title") (routeRequest router) ---- -- You may want to specify a custom document function instead: -- --
-- myDocument :: ByteString -> ByteString
-- myDocument content =
-- [i|<html>
-- <head>
-- <title>#{title}</title>
-- <script type="text/javascript">#{scriptEmbed}</script>
-- <style type type="text/css">#{cssResetEmbed}</style>
-- </head>
-- <body>#{content}</body>
-- </html>|]
--
basicDocument :: Text -> ByteString -> ByteString
-- | Route URL patterns to different pages
--
-- -- import Page.Messages qualified as Messages -- import Page.Users qualified as Users -- -- data AppRoute -- = Main -- | Messages -- | Users UserId -- deriving (Eq, Generic, Route) -- -- router :: (Hyperbole :> es) => AppRoute -> Eff es Response -- router Messages = page Messages.page -- router (Users uid) = page $ Users.page uid -- router Main = do -- view $ do -- el_ "click a link below to visit a page" -- route Messages id "Messages" -- -- main = do -- run 3000 $ do -- liveApp (basicDocument "Example") (routeRequest router) --routeRequest :: (Hyperbole :> es, Route route) => (route -> Eff es Response) -> Eff es Response instance GHC.Classes.Eq Web.Hyperbole.Application.SocketError instance GHC.Show.Show Web.Hyperbole.Application.SocketError module Web.Hyperbole.View -- | Element functions expect a Mod function as their first argument that -- adds attributes and classes. -- --
-- userEmail :: User -> View c () -- userEmail user = input (fontSize 16 . active) (text user.email) -- where -- active = isActive user then bold else id --type Mod = Attributes -> Attributes -- | Views are HTML fragments that carry all CSS used by any child -- element. -- --
-- view :: View c () -- view = col (pad 10 . gap 10) $ do -- el bold "Hello" -- el_ "World" ---- -- They can also have a context which can be used to create type-safe or -- context-aware elements. See table for an example data () => View context a data () => Align Center :: Align -- | Hexidecimal Color. Can be specified with or without the leading -- #. Recommended to use an AppColor type instead of manually -- using hex colors. See ToColor newtype () => HexColor HexColor :: Text -> HexColor -- | ToColor allows you to create a type containing your application's -- colors: -- --
-- data AppColor -- = White -- | Primary -- | Dark -- -- instance ToColor AppColor where -- colorValue White = "#FFF" -- colorValue Dark = "#333" -- colorValue Primary = "#00F" -- -- hello :: View c () -- hello = el (bg Primary . color White) "Hello" --class () => ToColor a colorValue :: ToColor a => a -> HexColor colorName :: ToColor a => a -> Text -- | Options for styles that support specifying various sides. This has a -- "fake" Num instance to support literals -- --
-- border 5 -- border (X 2) -- border (TRBL 0 5 0 0) --data () => Sides a All :: a -> Sides a TRBL :: a -> a -> a -> a -> Sides a X :: a -> Sides a Y :: a -> Sides a XY :: a -> a -> Sides a -- | Media allows for responsive designs that change based on -- characteristics of the window. See Layout Example data () => Media MinWidth :: Int -> Media MaxWidth :: Int -> Media -- | Milliseconds, used for transitions data () => Ms data () => PxRem data () => Length -- | Px, converted to Rem. Allows for the user to change the document font -- size and have the app scale accordingly. But allows the programmer to -- code in pixels to match a design PxRem :: PxRem -> Length Pct :: Float -> Length data () => TransitionProperty Width :: PxRem -> TransitionProperty Height :: PxRem -> TransitionProperty data () => Url Url :: Text -> Text -> [Segment] -> Query -> Url [$sel:scheme:Url] :: Url -> Text [$sel:domain:Url] :: Url -> Text [$sel:path:Url] :: Url -> [Segment] [$sel:query:Url] :: Url -> Query data () => TableColumn c dt data () => TableHead a -- | Add text to a view. Not required for string literals -- --
-- el_ $ do -- "Hello: " -- text user.name --text :: Text -> View c () -- | Apply to even-numbered children even :: Mod -> Mod -- | Cut off the contents of the element truncate :: Mod -- | Apply to odd-numbered children odd :: Mod -> Mod -- | A hyperlink to the given url link :: Url -> Mod -> View c () -> View c () value :: Text -> Mod -- | Space surrounding the children of the element -- -- To create even spacing around and between all elements: -- --
-- col (pad 10 . gap 10) $ do -- el_ "one" -- el_ "two" -- el_ "three" --pad :: Sides Length -> Mod style :: Text -> View c () -- | We can intuitively create layouts with combindations of row, -- col, grow, and space -- -- Wrap main content in layout to allow the view to consume -- vertical screen space -- --
-- holygrail :: View c () -- holygrail = layout id $ do -- row section "Top Bar" -- row grow $ do -- col section "Left Sidebar" -- col (section . grow) "Main Content" -- col section "Right Sidebar" -- row section "Bottom Bar" -- where section = border 1 --layout :: Mod -> View c () -> View c () name :: Text -> Mod -- | Space that fills the available space in the parent row or -- col. -- --
-- row id $ do -- space -- el_ "Right" ---- -- This is equivalent to an empty element with grow -- --
-- space = el grow none --space :: View c () -- | Apply when hovering over an element -- --
-- el (bg Primary . hover (bg PrimaryLight)) "Hover" --hover :: Mod -> Mod -- | Embed static, unescaped HTML or SVG. Take care not to use raw -- with user-generated content. -- --
-- spinner = raw "<svg>...</svg>" --raw :: Text -> View c () -- | Lay out children in a row -- --
-- row id $ do -- el_ "Left" -- space -- el_ "Right" --row :: Mod -> View c () -> View c () -- | Lay out children in a column. -- --
-- col grow $ do -- el_ "Top" -- space -- el_ "Bottom" --col :: Mod -> View c () -> View c () -- | Create a type safe data table by specifying columns -- --
-- usersTable :: [User] -> View c () -- usersTable us = do -- table id us $ do -- tcol (th hd "Name") $ \u -> td cell $ text u.name -- tcol (th hd "Email") $ \u -> td cell $ text u.email -- where -- hd = cell . bold -- cell = pad 4 . border 1 --table :: Mod -> [dt] -> Eff '[Writer [TableColumn c dt]] () -> View c () -- | Alternatively, the reset is available on a CDN -- --
-- import Data.String.Interpolate (i)
--
-- toDocument :: ByteString -> ByteString
-- toDocument cnt =
-- [i|<html>
-- <head>
-- <link rel="stylesheet" href="#{cssResetLink}">
-- </head>
-- <body>#{cnt}</body>
-- </html>|]
--
cssResetLink :: Text
-- | Set to a specific width
width :: Length -> Mod
-- | Set to a specific height
height :: Length -> Mod
-- | Allow width to grow to contents but not shrink any smaller than value
minWidth :: Length -> Mod
-- | Allow height to grow to contents but not shrink any smaller than value
minHeight :: Length -> Mod
-- | The space between child elements. See pad
gap :: Length -> Mod
fontSize :: Length -> Mod
-- | Set container to be a row. Favor row when possible
flexRow :: Mod
-- | Set container to be a column. Favor col when possible
flexCol :: Mod
-- | Adds a basic drop shadow to an element
shadow :: Mod
-- | Round the corners of the element
rounded :: Length -> Mod
-- | Set the background color. See ToColor
bg :: ToColor c => c -> Mod
-- | Set the text color. See ToColor
color :: ToColor c => c -> Mod
bold :: Mod
-- | Hide an element. See parent and media
hide :: Mod
opacity :: Float -> Mod
-- | Set a border around the element
--
-- -- el (border 1) "all sides" -- el (border (X 1)) "only left and right" --border :: Sides PxRem -> Mod -- | Set a border color. See ToColor borderColor :: ToColor c => c -> Mod -- | Use a button-like cursor when hovering over the element -- -- Button-like elements: -- --
-- btn = pointer . bg Primary . hover (bg PrimaryLight) -- -- options = row id $ do -- el btn "Login" -- el btn "Sign Up" --pointer :: Mod -- | Animate changes to the given property -- --
-- el (transition 100 (Height 400)) "Tall" -- el (transition 100 (Height 100)) "Small" --transition :: Ms -> TransitionProperty -> Mod textAlign :: Align -> Mod -- | Apply when the mouse is pressed down on an element active :: Mod -> Mod -- | Apply when the Media matches the current window. This allows for -- responsive designs -- --
-- el (width 100 . media (MinWidth 800) (width 400)) -- "Big if window > 800" --media :: Media -> Mod -> Mod -- | Apply when the element is somewhere inside an anscestor. -- -- For example, the HTMX library applies an "htmx-request" class to the -- body when a request is pending. We can use this to create a loading -- indicator -- --
-- el (pad 10) $ do -- el (parent "htmx-request" flexRow . hide) "Loading..." -- el (parent "htmx-request" hide . flexRow) "Normal Content" --parent :: Text -> Mod -> Mod pathUrl :: [Segment] -> Url cleanSegment :: Segment -> Segment pathSegments :: Text -> [Segment] url :: Text -> Url renderUrl :: Url -> Text -- | Get the current context context :: View context context -- | Run a view with a specific context in a parent View with -- a different context. This can be used to create type safe view -- functions, like table addContext :: context -> View context () -> View c () -- | Create a new element constructor -- --
-- aside :: Mod -> View c () -> View c () -- aside = tag "aside" --tag :: Text -> Mod -> View c () -> View c () -- | Set an attribute, replacing existing value -- --
-- hlink :: Text -> View c () -> View c () -- hlink url content = tag "a" (att "href" url) content --att :: Name -> AttValue -> Mod -- | Renders a View as HTML with embedded CSS class definitions -- --
-- >>> renderText $ el bold "Hello"
-- <style type='text/css'>.bold { font-weight:bold }</style>
-- <div class='bold'>Hello</div>
--
renderText :: View () () -> Text
renderLazyText :: View () () -> Text
renderLazyByteString :: View () () -> ByteString
-- | A basic element
--
-- -- el (bold . pad 10) "Hello" --el :: Mod -> View c () -> View c () -- | A basic element, with no modifiers -- --
-- el_ "Hello" --el_ :: View c () -> View c () -- | Do not show any content -- --
-- if isVisible -- then content -- else none --none :: View c () pre :: Mod -> Text -> View c () script :: Text -> View c () stylesheet :: Text -> View c () tcol :: forall dt c. View (TableHead c) () -> (dt -> View dt ()) -> Eff '[Writer [TableColumn c dt]] () th :: Mod -> View c () -> View (TableHead c) () td :: Mod -> View () () -> View dt () -- | As layout but as a Mod -- --
-- holygrail = col root $ do -- ... --root :: Mod -- | Grow to fill the available space in the parent row or -- col -- --
-- row id $ do -- el grow none -- el_ "Right" --grow :: Mod -- | Allow items to become smaller than their contents. This is not the -- opposite of grow! collapse :: Mod -- | Make a fixed layout by putting scroll on a child-element -- --
-- document = row root $ do -- nav (width 300) "Sidebar" -- col (grow . scroll) "Main Content" --scroll :: Mod -- | A Nav element nav :: Mod -> View c () -> View c () -- | Create fully interactive HTML applications with type-safe serverside -- Haskell. Inspired by HTMX, Elm, and Phoenix -- LiveView module Web.Hyperbole -- | Turn one or more Pages into a Wai Application. Respond using -- both HTTP and WebSockets -- --
-- main = do -- run 3000 $ do -- liveApp (basicDocument "Example") $ do -- page mainPage --liveApp :: (ByteString -> ByteString) -> Eff '[Hyperbole, Server, IOE] Response -> Application -- | Run an Application on the given port. This calls -- runSettings with defaultSettings. run :: Port -> Application -> IO () -- | Run a Page in Hyperbole page :: Hyperbole :> es => Page es Response -> Eff es Response -- | wrap HTML fragments in a simple document with a custom title and -- include required embeds -- --
-- liveApp (basicDocument "App Title") (routeRequest router) ---- -- You may want to specify a custom document function instead: -- --
-- myDocument :: ByteString -> ByteString
-- myDocument content =
-- [i|<html>
-- <head>
-- <title>#{title}</title>
-- <script type="text/javascript">#{scriptEmbed}</script>
-- <style type type="text/css">#{cssResetEmbed}</style>
-- </head>
-- <body>#{content}</body>
-- </html>|]
--
basicDocument :: Text -> ByteString -> ByteString
-- | Route URL patterns to different pages
--
-- -- import Page.Messages qualified as Messages -- import Page.Users qualified as Users -- -- data AppRoute -- = Main -- | Messages -- | Users UserId -- deriving (Eq, Generic, Route) -- -- router :: (Hyperbole :> es) => AppRoute -> Eff es Response -- router Messages = page Messages.page -- router (Users uid) = page $ Users.page uid -- router Main = do -- view $ do -- el_ "click a link below to visit a page" -- route Messages id "Messages" -- -- main = do -- run 3000 $ do -- liveApp (basicDocument "Example") (routeRequest router) --routeRequest :: (Hyperbole :> es, Route route) => (route -> Eff es Response) -> Eff es Response -- | Derive this class to use a sum type as a route. Constructors and -- Selectors map intuitively to url patterns -- --
-- data AppRoute -- = HomePage -- | Users -- | User Int -- deriving (Generic, Route) -- -- / -> HomePage -- /users/ -> Users -- /user/100 -> User 100 --class Route a -- | Convert a Route to a Url -- --
-- >>> routeUrl (User 100) -- /user/100 --routeUrl :: Route a => a -> Url -- | A hyperlink to another route -- --
-- >>> route (User 100) id "View User" -- <a href="/user/100">View User</a> --route :: Route a => a -> Mod -> View c () -> View c () -- | Hyperbole applications are divided into Pages. Each Page must -- load the whole page , and handle each type of -- HyperView -- --
-- myPage :: (Hyperbole :> es) => Page es Response -- myPage = do -- handle messages -- load pageView -- -- pageView = do -- el_ "My Page" -- hyper (Message 1) $ messageView "Starting Message" --data Page es a -- | The load handler is run when the page is first loaded. Run any side -- effects needed, then return a view of the full page -- --
-- myPage :: (Hyperbole :> es) => UserId -> Page es Response -- myPage userId = do -- load $ do -- user <- loadUserFromDatabase userId -- pure $ userPageView user --load :: Hyperbole :> es => Eff es (View () ()) -> Page es Response -- | A handler is run when an action for that HyperView is -- triggered. Run any side effects needed, then return a view of the -- corresponding type -- --
-- myPage :: (Hyperbole :> es) => Page es Response -- myPage = do -- handle messages -- load pageView -- -- messages :: (Hyperbole :> es, MessageDatabase) => Message -> MessageAction -> Eff es (View Message ()) -- messages (Message mid) ClearMessage = do -- deleteMessageSideEffect mid -- pure $ messageView "" -- -- messages (Message mid) (Louder m) = do -- let new = m <> "!" -- saveMessageSideEffect mid new -- pure $ messageView new --handle :: forall id es. (Hyperbole :> es, HyperView id) => (id -> Action id -> Eff es (View id ())) -> Page es () -- | HyperViews are interactive subsections of a Page -- -- Create an instance with a unique view id type and a sum type -- describing the actions the HyperView supports. The View Id can contain -- context (a database id, for example) -- --
-- data Message = Message Int -- deriving (Generic, Param) -- -- data MessageAction -- = Louder Text -- | ClearMessage -- deriving (Generic, Param) -- -- instance HyperView Message where -- type Action Message = MessageAction --class (Param id, Param (Action id)) => HyperView id where { type Action id :: Type; } -- | Embed HyperViews into the page, or nest them into other views -- --
-- myPage :: (Hyperbole :> es) => Page es Response -- myPage = do -- handle messages -- load $ do -- pure $ do -- el_ "My Page" -- hyper (Message 1) $ messageView "Hello World" -- hyper (Message 2) $ do -- messageView "Another Message" -- hyper OtherView otherView ---- -- Views can only trigger actions that match their HyperView -- --
-- messageView :: Text -> View Message () -- messageView m = do -- el_ (text m) -- button (Louder m) Louder -- -- otherView :: View OtherView () -- otherView = do -- -- Type Error! -- button (Louder "Hi") id Louder --hyper :: forall id ctx. HyperView id => id -> View id () -> View ctx () -- | <button> HTML tag which sends the action when pressed -- --
-- button SomeAction (border 1) "Click Me" --button :: HyperView id => Action id -> Mod -> View id () -> View id () -- | Type-safe dropdown. Sends (opt -> Action id) when selected. The -- selection predicate (opt -> Bool) controls which option is -- selected. See Example.Contacts -- --
-- data ContactsAction -- = Reload (Maybe Filter) -- | Delete Int -- deriving (Generic, Param) -- -- allContactsView :: Maybe Filter -> View Contacts () -- allContactsView fil = do -- row (gap 10) $ do -- el (pad 10) "Filter: " -- dropdown Reload (== fil) id $ do -- option Nothing "" -- option (Just Active) "Active!" -- option (Just Inactive) Inactive -- ... --dropdown :: HyperView id => (opt -> Action id) -> (opt -> Bool) -> Mod -> View (Option opt id (Action id)) () -> View id () -- | An option for a dropdown. First argument is passed to (opt -- -> Action id) in the dropdown, and to the selected predicate option :: (HyperView id, Eq opt) => opt -> View (Option opt id (Action id)) () -> View (Option opt id (Action id)) () -- | The view context for an option data Option opt id action -- | Give visual feedback when an action is in-flight. -- --
-- myView = do -- onRequest loadingIndicator $ do -- el_ "Loaded" -- where -- loadingIndicator = el_ "Loading..." --onRequest :: View id () -> View id () -> View id () -- | Send the action after N milliseconds. Can be used to implement lazy -- loading or polling -- --
-- pollMessageView :: Text -> View Message () -- pollMessageView m = do -- onLoad LoadMessage 1000 $ do -- el bold "Current Message. Reloading in 1s" -- el_ (text m) --onLoad :: HyperView id => Action id -> DelayMs -> View id () -> View id () type DelayMs = Int -- | Form Fields are identified by a type -- --
-- data User = User Text deriving (Generic, FormField) -- data Age = Age Int deriving (Generic, FormField) --class FormField a -- | Type-safe <form>. Calls (Action id) on submit -- --
-- userForm :: Validation -> View FormView () -- userForm v = do -- form Signup v id $ do -- el Style.h1 "Sign Up" -- -- field @User id Style.invalid $ do -- label "Username" -- input Username (placeholder "username") -- el_ invalidText -- -- field @Age id Style.invalid $ do -- label "Age" -- input Number (placeholder "age" . value "0") -- el_ invalidText -- -- submit (border 1) "Submit" --form :: forall id. HyperView id => Action id -> Validation -> Mod -> View (FormFields id) () -> View id () -- | Display a FormField -- --
-- data Age = Age Int deriving (Generic, FormField) -- -- myForm = do -- form SignUp mempty id $ do -- field @Age id id $ do -- label Age -- input Number (value "0") --field :: forall a id. FormField a => Mod -> Mod -> View (Input id a) () -> View (FormFields id) () -- | label for a field label :: Text -> View (Input id a) () -- | input for a field input :: InputType -> Mod -> View (Input id a) () -- | Button that submits the form. Use button to specify -- actions other than submit submit :: Mod -> View (FormFields id) () -> View (FormFields id) () placeholder :: Text -> Mod -- | Choose one for inputs to give the browser autocomplete hints data InputType NewPassword :: InputType CurrentPassword :: InputType Username :: InputType Email :: InputType Number :: InputType TextInput :: InputType Name :: InputType OneTimeCode :: InputType Organization :: InputType StreetAddress :: InputType Country :: InputType CountryName :: InputType PostalCode :: InputType Search :: InputType -- | Parse a FormField from the request -- --
-- formAction :: (Hyperbole :> es, UserDB :> es) => FormView -> FormAction -> Eff es (View FormView ()) -- formAction _ SignUp = do -- a <- formField @Age -- u <- formField @User -- saveUserToDB u a -- pure $ el_ "Saved!" --formField :: forall a es. (FormField a, Hyperbole :> es) => Eff es a -- | Validation results for a form -- --
-- validateUser :: User -> Age -> Validation -- validateUser (User u) (Age a) = -- validation -- [ validate @Age (a < 20) "User must be at least 20 years old" -- , validate @User (T.elem ' ' u) "Username must not contain spaces" -- , validate @User (T.length u < 4) "Username must be at least 4 chars" -- ] -- -- formAction :: (Hyperbole :> es, UserDB :> es) => FormView -> FormAction -> Eff es (View FormView ()) -- formAction _ SignUp = do -- a <- formField @Age -- u <- formField @User -- -- case validateUser u a of -- Validation [] -> successView -- errs -> userForm v ---- -- @ newtype Validation Validation :: [(Text, Text)] -> Validation -- | specify a check for a Validation validate :: forall a. FormField a => Bool -> Text -> Maybe (Text, Text) -- | Create a Validation from list of validators validation :: [Maybe (Text, Text)] -> Validation -- | Display any validation error for the FormField from the -- Validation passed to form -- --
-- field @User id Style.invalid $ do -- label "Username" -- input Username (placeholder "username") -- el_ invalidText --invalidText :: forall a id. FormField a => View (Input id a) () -- | In any load or handle, you can use this Effect to get -- extra request information or control the response manually. -- -- For most Pages, you won't need to use this effect directly. Use -- custom Routes for request info, and return Views to -- respond data Hyperbole :: Effect -- | Require a given parameter from the Query arguments -- --
-- myPage :: Page es Response -- myPage = do -- load $ do -- token <- reqParam "token" -- sideEffectUsingToken token -- pure myPageView --reqParam :: (Hyperbole :> es, FromHttpApiData a) => Text -> Eff es a -- | Return the entire Query -- --
-- myPage :: Page es Response -- myPage = do -- load $ do -- q <- reqParams -- case lookupParam "token" q of -- Nothing -> pure $ errorView "Missing Token in Query String" -- Just t -> do -- sideEffectUsingToken token -- pure myPageView --reqParams :: Hyperbole :> es => Eff es Query -- | Return all information about the Request request :: Hyperbole :> es => Eff es Request -- | Lookup the query param in the Query lookupParam :: ByteString -> Query -> Maybe Text -- | Return the request body as a Web.FormUrlEncoded.Form -- -- Prefer using Type-Safe Forms when possible formData :: Hyperbole :> es => Eff es Form -- | Respond immediately with 404 Not Found -- --
-- userLoad :: (Hyperbole :> es, Users :> es) => UserId -> Eff es User -- userLoad uid = do -- mu <- send (LoadUser uid) -- maybe notFound pure mu -- -- myPage :: (Hyperbole :> es, Users :> es) => Eff es View -- myPage = do -- load $ do -- u <- userLoad 100 -- -- skipped if user = Nothing -- pure $ userView u --notFound :: Hyperbole :> es => Eff es a -- | Redirect immediately to the Url redirect :: Hyperbole :> es => Url -> Eff es a -- | Respond with the given view, and stop execution respondEarly :: (Hyperbole :> es, HyperView id) => id -> View id () -> Eff es () -- | Lookup a session variable by keyword -- --
-- load $ do -- tok <- session "token" -- ... --session :: (Hyperbole :> es, FromHttpApiData a) => Text -> Eff es (Maybe a) -- | Set a session variable by keyword -- --
-- load $ do -- t <- reqParam "token" -- setSession "token" t -- ... --setSession :: (Hyperbole :> es, ToHttpApiData a) => Text -> a -> Eff es () -- | Clear the user's session clearSession :: Hyperbole :> es => Text -> Eff es () -- | Trigger actions for another view. They will update the view specified -- --
-- otherView :: View OtherView () -- otherView = do -- el_ "This is not a message view" -- button OtherAction id "Do Something" -- -- target (Message 2) $ do -- el_ "Now we can trigger a MessageAction which will update our Message HyperView, not this one" -- button ClearMessage id "Clear Message #2" --target :: HyperView id => id -> View id () -> View a () -- | Manually set the response to the given view. Normally you return a -- View from load or handle instead of using this view :: Hyperbole :> es => View () () -> Eff es Response -- | Types that can be serialized. HyperView requires this for both -- its view id and action -- --
-- data Message = Message Int -- deriving (Generic, Param) --class Param a toParam :: Param a => a -> Text toParam :: (Param a, Generic a, GParam (Rep a)) => a -> Text parseParam :: Param a => Text -> Maybe a parseParam :: (Param a, Generic a, GParam (Rep a)) => Text -> Maybe a -- | Valid responses for a Hyperbole effect. Use notFound, -- etc instead. Reminds you to use load in your Page -- --
-- myPage :: (Hyperbole :> es) => Page es Response -- myPage = do -- -- compiler error: () does not equal Response -- pure () --data Response -- | A constraint that requires that a particular effect e is a -- member of the type-level list es. This is used to -- parameterize an Eff computation over an arbitrary list of -- effects, so long as e is somewhere in the list. -- -- For example, a computation that only needs access to a mutable value -- of type Integer would have the following type: -- --
-- State Integer :> es => Eff es () --class () => (e :: Effect) :> (es :: [Effect]) -- | The Eff monad provides the implementation of a computation that -- performs an arbitrary set of effects. In Eff es a, -- es is a type-level list that contains all the effects that -- the computation may perform. For example, a computation that produces -- an Integer by consuming a String from the global -- environment and acting upon a single mutable value of type Bool -- would have the following type: -- --
-- (Reader String :> es, State Bool :> es) => Eff es Integer ---- -- Abstracting over the list of effects with (:>): -- --
-- app :: Application -- app req respond = bracket_ -- (putStrLn "Allocating scarce resource") -- (putStrLn "Cleaning up") -- (respond $ responseLBS status200 [] "Hello World") --type Application = Request -> Response -> IO ResponseReceived -> IO ResponseReceived -- | Representable types of kind *. This class is derivable in GHC -- with the DeriveGeneric flag on. -- -- A Generic instance must satisfy the following laws: -- --
-- from . to ≡ id -- to . from ≡ id --class () => Generic a