malinoff/elm-uniform/2.0.0

Uniform

Build in-place editable forms comprised of fields.

An in-place editable form is a form representing an existing entity where each individual field is in "Displaying" state initially, but can be switched to "Editing" state by clicking on it. The updated entity is usually being validated and returned onBlur.

If you're looking for a classic form which can represent a non-existing entity (like a sign up form) and which is comprised of editable fields, take a look at Uniform.Classic module.

Form definition

type Form values fields output

Unsurprisingly, a Form is a collection of fields which produces an output when filled with values. Of course, this does nothing by itself. It needs to have some fields!

type alias User =
    { name : String
    , age : Int
    }


type alias Values =
    { name : String
    , nameState : Uniform.FieldState
    , age : String
    , ageState : Uniform.FieldState
    }


type alias Fields =
    { name : Uniform.Field String String Values
    , age : Uniform.Field String Int Values
    }


type alias UserForm =
    Uniform.Form Values Fields User
succeed : fields -> output -> Form values fields output

Make a empty form with no fields that always successfully produces the given output.

Useful as a starting point to which you can append fields.

Fields

type FormField value output values

A value that knows how to fill a specific field with an appropriate value from values.

You shouldn't care about this type at all, it's exported so that Uniform.Classic.field can be implemented.

(value -> Bool)
-> FieldConfig value output values
-> FormField value output values

Define an actual field. It is not tied to a specific form yet, go and append it!

First argument is a function telling if the given value is empty. Useful for custom field types, but if you find yourself working mostly with text fields, take a look at textField.

Second argument is where you describe how to get and set the value, and how to parse it into the output.

ageField =
    Uniform.field
        String.isEmpty
        { parser = Result.fromMaybe "Not an integer" String.toInt
        , value = .age
        , updateValue = \values age -> { values | age = age }
        , state = Uniform.Displaying
        , updateState = \values state -> { values | ageState = state }
        }
type alias FieldConfig value output values =
{ parser : value -> Result String output
, value : values -> ( value, FieldState )
, update : values -> ( value, FieldState ) -> values
}

Describe how to get, set, parse the field's value and state.

  • parser specifies how to validate the value. It needs a function that processes the value of the field and produces a Result of either:
    • a String describing an error,
    • a correct output.
  • value specifies how to get the value and the state from values. Most of the time it will be in a form of .fieldName.
  • update specified how to update values with a new value and/or a new state. Most of the time it will look like \values value -> { values | fieldName = value }
FormField value fieldOutput values
-> Form
values
(Field value fieldOutput values -> fields)
(fieldOutput -> output)
-> Form values fields output

Take a field and append it to the form.

userForm =
    Uniform.succeed User Fields
        |> Uniform.append nameField
        |> Uniform.append ageField
type alias TextField output values =
Field String output values

Lots of fields, however, are just plain strings with custom validation. This alias is not mandatory to use, but can help to shorten your fields definition a bit.

It is intentional that there are no other similar aliases in the library. I encourage you to define your own aliases for your custom value types.

FieldConfig String output values
-> FormField String output values

Define a text field. Not mandatory to use, but can help to shorten your fields definition a bit.

All it does is calling field String.isEmpty.

It is intentional that there are no other similar helpers in the library. I encourage you to implement similar helper functions for your custom value types.

FormField value output values
-> FormField value (Maybe output) values

All fields are required by default, but you can make them optional.

Uniform.succeed SomeOutput SomeFields
    |> Uniform.append field1
    |> Uniform.append (optional field2)

The corresponding field in your output model must be Maybe output instead of just output.

(values -> FormField value output values)
-> FormField value output values

This function allows fields to peek into other fields values.

You can use it for plenty different use cases:

  • define different fields (with possibly different value types) depending on the value of an other field,
  • extend parser capabilities with parsing dependent values
  • extend parser capabilities with an external data (who said that values should only represent form values? Nothing stops you from defining an extra field that you can inspect later).
fieldWithComplexValidation =
    Uniform.dynamicField
        \values ->
            Uniform.textField
                { parser = complexParser values
                , ...
                }

Outputs

Form values fields output
-> values
-> { fields : fields
, output : Maybe output
}

Once you have your form defined and fields appended, you can now fill it with values.

If all values are properly parsed into fields, the returned output field will contain Just output, otherwise Nothing. You can inspect individual fields from the returned fields, each one will have a type of Field.

However, I would advise not to implement your view for the form by inspecting the returned fields directly. It is error-prone: it is easy to forget to branch on the state, or to call wrong update functions. viewField function is a type-safe way to view fields, it should cover all your needs.

type alias Field value output values =
{ value : value
, updateValue : value -> values
, state : FieldState
, updateState : values
, output : Result Error output
}

Represents a field filled with values.

  • value contains the "raw" value that can be displayed in a view,
  • updateValue is a function that is usually used in an onInput event,
  • state contains the current state of the field,
  • updateState is a function that is usually used in onClick/onBlur events,
  • output contains either a successfully parsed output or an error.
= Editing
| Displaying

Each field in an in-place editable form can be either Editing or Displaying. I hope these names are obvious :) If not, submit a PR!

type Error
= RequiredFieldIsEmpty
| ValidationFailed String

This represents validation errors that may happen during filling a form. Again, naming should be obvious. If not, submit a PR!

mapOutput : (a -> b) -> Form values fields a -> Form values fields b

Transform the output of the form.

This function can help you to keep forms decoupled from specific view messages:

Uniform.mapOutput SignUp userForm

View

The form definition is completely decoupled from its view. You are responsible for implementing views for your form; as there are no two identical forms, I don't even try to provide a "universal, configurable" solution to this problem.

However, I can provide some helpers so that it's easy to display the form's fields properly. If these helpers don't work for you, opt-out and inspect the filled form's fields directly.

ViewConfig value values element
-> Field value output values
-> element

A type-safe way to view a single field from the filled form's fields.

viewNameField =
    Uniform.viewField
        { viewWhenDisplaying = ...
        , viewWhenEditing = ...
        }


viewAgeField =
    Uniform.viewField
        { viewWhenDisplaying = ...
        , viewWhenEditing = ...
        }


viewForm model =
    let
        filled =
            Uniform.fill userForm model.formValues
    in
    Html.div
        []
        [ viewNameField
        , viewAgeField
        ]
type alias ViewConfig value values element =
{ viewWhenDisplaying :
{ value : value
, makeEditable : values
}
-> element
, viewWhenEditing :
{ value : value
, updateValue : value -> values
, finishEditing : values
}
-> element
}

Describe how to view a form's field in a proper state.

  • viewWhenDisplaying is a function that is called when the field is in Displaying state. Accepts the value to display and the makeEditable function that should be called in onClick handler like this:

    viewWhenDisplaying { value, makeEditable } =
        Html.div
            [ Html.Events.onClick (FormUpdated makeEditable) ]
            [ Html.text value ]
  • viewWhenEditing is a function that is called when the field is in Editing state. Accepts the value to display, the updateValue function that should be called in onInput handler, and the finishEditing function that should be called in onBlur handler like this:

    viewWhenEditing { value, updateValue, finishEditing } =
        Html.input
            [ Html.Events.onBlur (FormUpdated finishEditing)
            , Html.Events.onInput (FormUpdated << updateValue)
            , Html.Attributes.value value
            , Html.Attributes.type\_ "text"
            []