Hands-on Laminar
By: Rafał Zelek
Tags: Frontend Development, Laminar, Scala, Software Development

Forms

Inside the laminar documentation (https://laminar.dev/examples/uncontrolled-inputs), there are three examples of how to manage forms.

I want to explore some React-like approach, in which each component is an independent reusable piece of code. On top of that, we will use fancy Scala features to extend our domain language and make the code easier to reason about.

Requirements

The job is to implement a simple registration form:

There are 3 mandatory fields: e-mail, password, repeated password.

Each field should be validated.

An e-mail should contain the @ sign.

sign. A password should be nonempty.

A repeated password should match the password.

The form is validated on leaving a field or on pressing a register button.

Styling

For styling purposes, I’ll use the ScalaCSS library, which was already mentioned above. I want to have some pretty animation to make that form a little bit alive and that’s why the labels will serve us as a custom placeholder that goes to the left top corner when a user starts the interaction with the input field. It’s a similar approach to the one in material design.

The styles are nothing fancy, that’s why I’ll just drop the code as a reference, without digging into too many details.

First of all, let’s define styles for a form, header, and register button.

Now the input fields. I treat them separately because the mechanism of “material-like” labels can be reused across different parts of the system.

To move the label away to the top-left corner, we just add non-empty class to the input field, that’s why we need to use unsafeChild style definitions.

We also need syntactic sugar, which we defined previously because we will use it excessively.

Form skeleton

Let’s create a bare-bones visual implementation that we will improve in the next steps.

Pretty long code for such a simple task, isn’t it?

But anyway, the only moving parts are reactive signal s of 3 Var iables to add non-empty label into the fields.

This mechanism is pretty generic; write into Var when an event occurs, display class based on Var value:

It’s such a common operation that laminar provides syntactic sugar syntax for it! You can simply write:

In that big chunk of code a few lines above, there is a lot of repetition over here, so let’s break it down into smaller components and share these moving parts.

Input fields extracted

Starting from a top-level perspective, the only things that are different between these input fields are: id , name , type and state Var iable.

Having that in mind, we can create a simple API that abstracts away the implementation details we repeated:

Input fields implementation details:

The functionality is the same, but we managed to reduce some duplication.

Notice how Observer is used here to propagate changes from children to the parent.

Observer[A] is a modest wrapper around an onNext: A => Unit callback that represents an external observer

(source: https://github.com/raquo/Airstream#observer)

This is a similar pattern we have in React for smart and dumb components, you can treat that as a simple callback. We want to keep the Input as dumb as possible.

Even though the Input has internally state binding between onInput value and cls.toggle , it’s still for purely visual purposes. The only thing that component gives back is a value returned via valueObserver: Observer[String] , and it’s his parent job to do something with that state.

Form validation

So far so good, we have something, but it’s still a long road to fulfilling all the requirements.

Validation can be triggered by different actions. It can be done on blur, on form submit, or on change of other input value. That’s why we will use a Signal[A] to reactively update children with validation status.

The idea behind this type of alias definition ( type Validator = Signal[Option[String]] ) is that the signal generic type Option[String] returns Some("error text") when validation fails or None when it succeeds.

We add validators: Validator* var args to both Email and Password components for nice API. It’s var args because there might be different validations at the same time. E.g. password pattern with each requirement in a different line. Errors component can display more than 1 error for the same reason.

Function Signal.combineSeq , from the code below, simply changes Seq[Signal[A]] into Signal[Seq[A]] .

Remember to replace the Email component usage with mocked error:

The styles were already defined for that functionality, so without any further changes, the form should look like this:

Fully functional form validation

We have the visual part done.

The parent ( Forms object) needs to know the internal state of the input components to make proper validation on form submit and on blur.

To do that, we will use Observer[A] again.

Not all of them are used in our case, but I’ll show how to implement every input state value.

Let’s define a config class that we’ll pass into the Input component.

Now, the heart of state binding. Laminar gives us the possibility to create a separate Mod instance and just use it as a parameter to an HTML element.

$invalid: Signal[Boolean] is passed as a parameter, because it’s already calculated for the CSS invalid class.

Password and Email just accept the configuration and pass it into the Input component to use it as a parameter to inputStateMod , which again will bind our input state with the parent via InputStateConfig .

Form validation wrap-up

Everything we already did is just dumb visual components composition with callback bindings. Let’s get our hands dirty and connect everything together.

First, we need to implement simple pure validation functions according to our requirements.

Let’s redefine Email method in Forms object to use new Email input component with validators and input config.

According to our requirements, the validation is triggered when a user tries to send a form or when they touch the field. The caller ( RegisterForm method) has the state of the whole form and knows when the form is sent. The Email method knows when the e-mail input field was touched.

When one of these two Signal s is triggered with a true value, the form should be validated, but only when there is anything to validate. To achieve that, we will combine $email.signal , $formSend , and $emailTouched.signal and return the validation result if any of them is true . This composition will block the validation until the user makes any action, we don’t want to show errors on the not touched form, right?

There is also a contramap method which can be new for you. In simple words, this function will convert a value from $email into $emailObserver and return None when the e-mail value is invalid.

The docs will describe it perfectly, so I’ll just leave it as a reference.

Creates another Observer such that calling its onNext will call this observer’s onNext with the value processed by the project function.

This is useful when you need to pass down an Observer[A] to a child component which should not know anything about the type A, but both child and parent know about type B, and the parent knows how to translate B into A.

def contramap[B](project: B => A): Observer[B]

In my opinion, this code looks terrible! We can’t really extend the library API, so let’s make it cleaner with some fancy Scala extension methods!

Please notice how we used the Observer.combine function to combine Observer s that listen to our e-mail value changes.

Moving back to the above code.

First of all, let’s get rid of this terrible contramap . We intend to emit a value ONLY when the isEmailValid function returns None (which means the email was valid and no error was returned).

Using that vocabulary, we can create a high-level API. $emailObserver.emitIfValid(isEmailValid) would look pretty nice.

$emailObserver is of type Observer[Option[A]] , we accept the validator of type A => Option[A] to finally apply contramap .

Now the Email looks like this:

The code looks much better, but we still have this ugly part related to input validation.

This piece of code inside map mixes two things.

First, applying a boolean operation. Second, filtering out the validation result when none of them is true .

With two simple implicit conversions, we can extend the Signal to create a nice and lean API.

The final Email looks like this

Let’s move on to two password fields: