malinoff / elm-uniform / 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.

field : (value -> Basics.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.

append : 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.

textField : 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.

optional : 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.

dynamicField : (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:

fieldWithComplexValidation =
    Uniform.dynamicField
        \values ->
            Uniform.textField
                { parser = complexParser values
                , ...
                }

Outputs

fill : 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.


type FieldState
    = 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.

viewField : 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.