packages feed

nano-ui-form-0.1.0.0: lib/NanoUI/Form/Runner.hs

-- | Running a form in a view: live validation, a submit button, a configured
-- runner, a deferred view, and reset.
module NanoUI.Form.Runner
  ( runNanoForm
  , nanoFormLive
  , nanoFormSubmit
  , nanoFormEx
  , resetForm
  ) where

import Control.Monad (when)
import Data.Text (Text)
import qualified Ditto.Core as Ditto
import qualified Ditto.Types as Ditto
import NanoUI
  ( Key (KeyEnter)
  , NanoUI
  , button
  , column
  , inputKeys
  , inputKeysElem
  , uiIO
  , whenM
  )
import NanoUI.Monad (askContext, askInput)
import NanoUI.Form.Backend
  ( FormUI (..)
  , isFormSubmitted
  , markFormSubmitted
  , resetFormState
  , withFormPrefix
  , withFormWidgets
  )
import NanoUI.Form.Types
  ( Form
  , FormConfig (..)
  , FormMode (..)
  , FormStatus (..)
  , FormView (..)
  , defaultFormConfig
  )

-- | Evaluate a formlet and return its view and result. The view retains its
-- prefix even when rendered after other forms or inside another form's view.
runNanoForm :: Text -> Form err a -> NanoUI (Ditto.View err FormView, Ditto.Result err (Ditto.Proved a))
runNanoForm prefix form = withNanoForm prefix form $ \view result -> do
  let scopedView (FormView action) = FormView (withFormPrefix prefix action)
  pure (scopedView <$> view, result)

-- Immediate runners evaluate and render in one prefix scope. Only a deferred
-- view returned by runNanoForm needs to re-enter that scope when it is rendered.
withNanoForm ::
  Text
  -> Form err a
  -> (Ditto.View err FormView -> Ditto.Result err (Ditto.Proved a) -> NanoUI b)
  -> NanoUI b
withNanoForm prefix form consume = withFormPrefix prefix $ do
  (view, result) <- unFormUI (Ditto.runForm prefix form)
  let keyedView (FormView action) = FormView (withFormWidgets prefix action)
  consume (keyedView <$> view) result

-- | Default form runner: renders the form every frame with live validation
-- and yields @Just a@ whenever it is valid.
nanoFormLive :: Text -> Form Text a -> NanoUI (Maybe a)
nanoFormLive prefix form = do
  status <- nanoFormEx defaultFormConfig prefix form
  pure $ case status of
    FormValid a -> Just a
    FormInvalid _ -> Nothing

-- | Run a form with an integrated submit button.
-- Validation errors are only displayed after the first submission attempt.
-- Returns @Just a@ only on a valid submission.
nanoFormSubmit :: Text -> Text -> Form Text a -> NanoUI (Maybe a)
nanoFormSubmit prefix submitLabel form = do
  ctx <- askContext
  inp <- askInput
  submittedBefore <- uiIO (isFormSubmitted ctx prefix)
  withNanoForm prefix form $ \view' res -> do
    btnClicked <- column $ do
      renderResult submittedBefore view' res
      button submitLabel
    let enterPressed = inputKeysElem KeyEnter (inputKeys inp)
        clickedSubmit = btnClicked || enterPressed
    when clickedSubmit $
      uiIO (markFormSubmitted ctx prefix True)
    pure $ case (clickedSubmit, res) of
      (True, Ditto.Ok (Ditto.Proved _ a)) -> Just a
      _                                  -> Nothing

-- | Detailed form runner with custom configuration.
nanoFormEx :: FormConfig -> Text -> Form Text a -> NanoUI (FormStatus a)
nanoFormEx cfg prefix form = do
  ctx <- askContext
  submittedBefore <- uiIO (isFormSubmitted ctx prefix)
  withNanoForm prefix form $ \view' res -> do
    let showErrors = case fcMode cfg of
          FormLive     -> True
          FormOnSubmit -> submittedBefore
    column $ do
      renderResult showErrors view' res
      case fcSubmitButton cfg of
        Just lbl ->
          whenM (button lbl) $
            uiIO (markFormSubmitted ctx prefix True)
        Nothing -> pure ()
    pure $ case res of
      Ditto.Ok (Ditto.Proved _ a) -> FormValid a
      Ditto.Error errs -> FormInvalid errs

renderResult :: Bool -> Ditto.View err FormView -> Ditto.Result err a -> NanoUI ()
renderResult showErrors view result =
  runFormView (Ditto.unView view errorsToShow)
  where
    errorsToShow = case result of
      Ditto.Error errs | showErrors -> errs
      _ -> []

-- | Reset input values and the corresponding widget state for a form prefix.
-- Re-evaluate the form on the next frame to render its defaults.
resetForm :: Text -> NanoUI ()
resetForm prefix = do
  ctx <- askContext
  uiIO (resetFormState ctx prefix)