Skip to content

Commit

Permalink
feat: add support for ReadOnly form/field
Browse files Browse the repository at this point in the history
[Fable.Form][Fable.Form.Simple][Fable.Form.Simple.Bulma]

=== changelog ===
1. Set it at the field level

    ```fsharp
    Form.textField
        // ...
        |> Form.readOnly

    // or

    Form.textField
        // ...
        |> Form.readOnlyIf myCondition
    ```

2. Set it at the form level

    ```fsharp
    let formValue : Form.View.Model<Values> = // ...

    { formValue with State = Form.View.State.Loading }
    ```
=== changelog ===
  • Loading branch information
MangelMaxime committed Sep 9, 2024
1 parent 28337d9 commit 02e31e6
Show file tree
Hide file tree
Showing 20 changed files with 444 additions and 40 deletions.
1 change: 1 addition & 0 deletions examples/Examples.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<Compile Include="src/Page/ValidationStrategies.fs" />
<Compile Include="src/Page/CustomActions.fs" />
<Compile Include="src/Page/CustomField.fs" />
<Compile Include="src/Page/Disable.fs" />
<Compile Include="src/Page/Composability/Simple/AddressForm.fs" />
<Compile Include="src/Page/Composability/WithConfiguration/AddressForm.fs" />
<Compile Include="src/Page/Composability/Simple.fs" />
Expand Down
29 changes: 29 additions & 0 deletions examples/src/App.fs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module ComposabilitySimple = Page.Composability.Simple.Component
module ComposabilityWithConfiguration = Page.Composability.WithConfiguration.Component
module CustomAction = Page.CustomAction.Component
module CustomField = Page.CustomField.Component
module Disable = Page.Disable.Disable

[<RequireQualifiedAccess>]
[<NoComparison>]
Expand All @@ -37,6 +38,7 @@ type Page =
| ComposabilityWithConfiguration of ComposabilityWithConfiguration.Model
| CustomAction of CustomAction.Model
| CustomField of CustomField.Model
| Disable of Disable.Model
| NotFound

[<NoComparison>]
Expand All @@ -52,6 +54,7 @@ type Msg =
| ComposabilityWithConfigurationMsg of ComposabilityWithConfiguration.Msg
| CustomActionMsg of CustomAction.Msg
| CustomFieldMsg of CustomField.Msg
| DisableMsg of Disable.Msg

[<NoComparison>]
type Model =
Expand Down Expand Up @@ -155,6 +158,14 @@ let private setRoute (optRoute: Router.Route option) (model: Model) =
},
Cmd.map CustomFieldMsg subCmd

| Router.Route.Disable ->
let (subModel, subCmd) = Disable.init ()

{ model with
ActivePage = Page.Disable subModel
},
Cmd.map DisableMsg subCmd

| Router.Route.Home ->
{ model with
ActivePage = Page.Home
Expand Down Expand Up @@ -311,6 +322,20 @@ let private update (msg: Msg) (model: Model) =

| _ -> model, Cmd.none

| DisableMsg subMsg ->
match model.ActivePage with
| Page.Disable subModel ->
Disable.update subMsg subModel
|> Tuple.mapFirst Page.Disable
|> Tuple.mapFirst (fun page ->
{ model with
ActivePage = page
}
)
|> Tuple.mapSecond (Cmd.map DisableMsg)

| _ -> model, Cmd.none

let private init (location) =
setRoute
location
Expand Down Expand Up @@ -437,6 +462,7 @@ let private contentFromPage (page: Page) (dispatch: Dispatch<Msg>) =
renderLink FormList.information
renderLink ComposabilitySimple.information
renderLink ComposabilityWithConfiguration.information
renderLink Disable.information
]

Bulma.content [
Expand Down Expand Up @@ -504,6 +530,9 @@ let private contentFromPage (page: Page) (dispatch: Dispatch<Msg>) =
CustomField.information
(CustomField.view subModel (CustomFieldMsg >> dispatch))

| Page.Disable subModel ->
renderDemoPage Disable.information (Disable.view subModel (DisableMsg >> dispatch))

| Page.NotFound -> Html.text "Page not found"

let private view (model: Model) (dispatch: Dispatch<Msg>) =
Expand Down
2 changes: 1 addition & 1 deletion examples/src/Fields/SignatureField.fs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ module SignatureField =
style.border (1, borderStyle.solid, "hsl(0, 0%, 86%)")
// If the field is disabled, we can disable pointer events to
// make the canvas not interactable
if config.Disabled then
if config.Disabled || config.IsReadOnly then
style.backgroundColor "hsl(0, 0%, 96%)"
style.pointerEvents.none
]
Expand Down
3 changes: 0 additions & 3 deletions examples/src/Page/Composability/WithConfiguration.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ open Elmish
open Fable.Form.Simple
open Fable.Form.Simple.Bulma

// Student
// Teacher

/// <summary>
/// Represent the form values
/// </summary>
Expand Down
261 changes: 261 additions & 0 deletions examples/src/Page/Disable.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
module Page.Disable.Disable

open Elmish
open Fable.Form.Simple
open Fable.Form.Simple.Bulma
open Feliz
open Feliz.Bulma

/// <summary>
/// Type used to represent the form values
/// </summary>
type Values =
{
Email: string
Password: string
Biography: string
}

/// <summary>
/// Represents the model of your Elmish component
/// </summary>
type Model =
| FillingForm of Form.View.Model<Values>
| FilledForm of string * string * string

/// <summary>
/// Represents the different messages that your application can react too
/// </summary>
type Msg =
// Used when a change occur in the form
| FormChanged of Form.View.Model<Values>
// Used when the user submit the form
| Submit of string * string * string
// Used when the user ask to reset the demo
| ResetTheDemo

let init () =
{
Email = ""
Password = ""
Biography = "You can't edit me :)"
}
|> Form.View.idle
|> FillingForm,
Cmd.none

let update (msg: Msg) (model: Model) =
match msg with
// Update our model to it's new state
| FormChanged newModel ->
match model with
| FillingForm _ -> FillingForm newModel, Cmd.none

| FilledForm _ -> model, Cmd.none

| Submit(_email, _password, _rememberMe) ->
match model with
| FillingForm _ -> FilledForm(_email, _password, _rememberMe), Cmd.none

| FilledForm _ -> model, Cmd.none

| ResetTheDemo -> init ()

/// <summary>
/// Define the form logic
///
/// We need to define each field logic first and then define how the fields are wired together to make the form
/// </summary>
/// <returns>The form ready to be used in the view</returns>
let form: Form<Values, Msg> =
let emailField =
Form.textField
{
Parser = Ok
Value = fun values -> values.Email
Update =
fun newValue values ->
{ values with
Email = newValue
}
Error = fun _ -> None
Attributes =
{
FieldId = "email"
Label = "Email"
Placeholder = "[email protected]"
HtmlAttributes =
[
prop.autoComplete "email"
]
}
}

let passwordField =
Form.meta (fun values ->
Form.passwordField
{
Parser = Ok
Value = fun values -> values.Password
Update =
fun newValue values ->
{ values with
Password = newValue
}
Error = fun _ -> None
Attributes =
{
FieldId = "password"
Label = "Password (disabled when email is empty)"
Placeholder = "Your password"
HtmlAttributes =
[
prop.autoComplete "current-password"
]
}
}
|> Form.disableIf (values.Email.Length = 0)
)

let biographyField =
Form.textareaField
{
Parser = Ok
Value = fun values -> values.Biography
Update =
fun newValue values ->
{ values with
Biography = newValue
}
Error = fun _ -> None
Attributes =
{
FieldId = "biography"
Label = "Biography (always disabled)"
Placeholder = "Tell us about yourself"
HtmlAttributes = []
}
}
|> Form.disable

/// <summary>
/// Function used to map the form values into the message to send back to the update function
/// </summary>
/// <returns></returns>
let onSubmit = fun email password biography -> Submit(email, password, biography)

Form.succeed onSubmit
|> Form.append emailField
|> Form.append passwordField
|> Form.append biographyField

let private renderRow (leftValue: string) (rightValue: string) =
Html.tr [
Html.td leftValue
Html.td rightValue
]

let private renderFilledView
(email: string)
(password: string)
(biography: string)
(dispatch: Dispatch<Msg>)
=
Bulma.content [

Bulma.message [
color.isSuccess

prop.children [
Bulma.messageBody [
prop.text "You have successfully filled the form"
]
]
]

Bulma.table [
table.isStriped

prop.children [
Html.thead [
Html.tr [
Html.th "Field"
Html.th "Value"
]
]

Html.tableBody [
renderRow "Email" email
renderRow "Password" password
renderRow "Biography" biography
]
]

]

Bulma.text.p [
text.hasTextCentered

prop.children [
Bulma.button.button [
prop.onClick (fun _ -> dispatch ResetTheDemo)
color.isPrimary

prop.text "Reset the demo"
]
]
]

]

let view (model: Model) (dispatch: Dispatch<Msg>) =
match model with
| FillingForm values ->
Html.div [
Bulma.message [
color.isInfo
prop.children [
Bulma.messageBody [
Html.div
"This is a silly form that will disable the password field when the email is empty and lock the biography field"
]
]
]

Form.View.asHtml
{
Dispatch = dispatch
OnChange = FormChanged
Action = Form.View.Action.SubmitOnly "Sign in"
Validation = Form.View.ValidateOnSubmit
}
form
values
]

| FilledForm(email, password, biography) -> renderFilledView email password biography dispatch

let information: DemoInformation.T =
{
Title = "Disable"
Route = Router.Route.Disable
Description = "Demonstrate how to disable a field (same logic can be applied for read-only)"
Remark = None
Code =
"""
Form.textField
{
// ...
}
|> Form.disable
// or
Form.textField
{
// ...
}
|> Form.disableIf myCondition
"""
GithubLink = Env.generateGithubUrl __SOURCE_DIRECTORY__ __SOURCE_FILE__
}
1 change: 0 additions & 1 deletion examples/src/Page/Login.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module Page.Login.Component
open Elmish
open Fable.Form.Simple
open Fable.Form.Simple.Bulma
open Fable.Form.Simple.Bulma.Fields
open Feliz

/// <summary>
Expand Down
3 changes: 3 additions & 0 deletions examples/src/Router.fs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Route =
| ValidationStrategies
| CustomAction
| CustomField
| Disable
| NotFound

let private toHash page =
Expand All @@ -45,6 +46,7 @@ let private toHash page =
| Route.ValidationStrategies -> "validation-strategies"
| Route.CustomAction -> "custom-actions"
| Route.CustomField -> "custom-field"
| Route.Disable -> "disable"
| Route.FormList -> "form-list"

"#" + segmentsPart
Expand All @@ -64,6 +66,7 @@ let routeParser: Parser<Route -> Route, Route> =
(Route.Composability ComposabilityRoute.WithConfiguration)
(s "composability" </> s "with-configuration")
map Route.CustomField (s "custom-field")
map Route.Disable (s "disable")
map Route.Home top
]

Expand Down
Loading

0 comments on commit 02e31e6

Please sign in to comment.