Skip to content
scalive
Menu
ConnectingLiveReconnectingOffline

Typed profile form

Change individual fields to reveal only their relevant validation feedback, or submit the form to validate every field. Valid input is decoded into a typed Profile; reset returns the nested LiveView to pristine state.

LiveView
scala
final class ProfileFormExample extends LiveView[ProfileFormExample.Msg, ProfileFormExample.Model]:
  import ProfileFormExample.*

  def mount(ctx: MountContext): Task[Model] =
    ZIO.succeed(Model(Profile.Definition.initial()))

  def handleMessage(model: Model, ctx: MessageContext) =
    case Msg.Validate(event) =>
      ZIO.succeed(model.copy(form = Profile.Definition.from(event), saved = None))
    case Msg.Save(event) =>
      ZIO.succeed(
        model.copy(
          form = Profile.Definition.from(event),
          saved = event.value.toOption
        )
      )
    case Msg.Reset =>
      ZIO.succeed(Model(Profile.Definition.initial()))

  override def view(model: Signal[Model]): HtmlElement[Msg] =
    val profileForm    = model.map(_.form)
    val nameField      = profileForm.field(Profile.Name)
    val emailField     = profileForm.field(Profile.Email)
    val biographyField = profileForm.field(Profile.Biography)

    div(
      cls := "docs-profile-form",
      model.map(_.saved).option { profile =>
        p(
          dataAttr("profile-saved") := "",
          cls                       := "docs-profile-saved",
          role                      := "status",
          profile.map(profile => s"Saved ${profile.name}'s profile.")
        )
      },
      form(
        dataAttr("profile-form") := "",
        Profile.Definition.onChange(Msg.Validate(_)),
        Profile.Definition.onSubmit(Msg.Save(_)),
        field(
          label(forId := nameField.id, "Name"),
          nameField.text(
            nameField.validationAttributes,
            placeholder := "Ada Lovelace"
          ),
          nameField.errorFeedback(dataAttr("field-error") := "name")
        ),
        field(
          label(forId := emailField.id, "Email"),
          emailField.email(
            emailField.validationAttributes,
            placeholder := "ada@example.com"
          ),
          emailField.errorFeedback(dataAttr("field-error") := "email")
        ),
        field(
          label(forId := biographyField.id, "Biography"),
          biographyField.textarea(
            biographyField.validationAttributes,
            rows        := 5,
            placeholder := s"Up to ${Profile.BiographyMaxLength} characters"
          ),
          biographyField.errorFeedback(dataAttr("field-error") := "biography")
        ),
        div(
          cls := "docs-profile-actions",
          button(typ := "submit", submission.replaceTextWith("Saving..."), "Save profile"),
          button(typ := "button", on.click(Msg.Reset), "Reset form")
        )
      )
    )
  end view

  private def field(content: Mod[Msg]*): HtmlElement[Msg] =
    div(cls := "docs-profile-field", content)
end ProfileFormExample

object ProfileFormExample:
  final case class Profile(name: String, email: String, biography: String)

  object Profile:
    val BiographyMaxLength = 500
    val Root               = FormRoot("profile")

    val Name = Root
      .string("name")
      .map(_.trim)
      .required("Name is required.")

    val Email = Root
      .string("email")
      .map(_.trim)
      .required("Email is required.")
      .validate("Enter a valid email address.")(EmailPattern.matches)

    val Biography = Root
      .string("biography")
      .map(_.trim)
      .required("Biography is required.")
      .validate(s"Biography must be $BiographyMaxLength characters or fewer.")(
        _.length <= BiographyMaxLength
      )

    val Definition = Root.form(Profile.apply)(Name, Email, Biography)

    private val EmailPattern = """^[^\s@]+@[^\s@]+\.[^\s@]+$""".r

  final case class Model(form: Profile.Definition.Form, saved: Option[Profile] = None)

  enum Msg:
    case Validate(event: FormEvent[Profile])
    case Save(event: FormEvent[Profile])
    case Reset
end ProfileFormExample

Result

ConnectedReconnectingRead-only

Disconnected. Controls resume after reconnection.

UnavailableConnect to capture interactions

Related guidance: build typed forms and validation.