GithubLinkedIn

Fun with scalajs lenses and react

scalareact

2015-06-16

I've been working (freelance) on a single page app using scalajs-react

and lenses helped me cleanup some code. They were fun and straightforward to get started with, so I wanted to quickly share my experience.

The model

Lets say our app has some configuration that can be edited by the user. We'll model it with some case classes:

1  case class Config(restricted: String, mailConfig:MailConfig)
2
3  object Config{
4
5    lazy val default = Config("thrownforaloop.com",MailConfig("localhost",9000,"mike"))
6  }
7
8  case class MailConfig(
9    host:String,
10    port:Int,
11    user:String
12  )
13

The editable form

Lets make a few react components so that we can construct our form. We'll add bootstrap classes to make it a littler more friendly.

1  object Form{
2    case class Field(name:String,value:String,onChange:ReactEventI=>Unit)
3
4
5    val field = ReactComponentB[Field]("form-field")
6      .render(P=>{
7        val Field(name,value,onChange) = P
8        <.div(^.`class`:="form-group",
9          <.label(
10            ^.`for` :=name,
11            name.capitalize),
12          <.input(
13            ^.tpe := "text",
14            ^.`class`:="form-control",
15            ^.id := name,
16            ^.value := value,
17            ^.onChange ==> onChange
18          ))
19      })
20      .build
21
22    val form = ReactComponentB[Seq[Field]]("form")
23      .render(P=>{
24        <.div(^.`class`:="panel panel-default",
25          <.div(^.`class`:="panel",
26            <.div(^.`class`:="panel-heading",
27              "Config Edit"
28            ),
29            <.div(^.`class`:="panel-body",
30              <.form(
31                P.map(f => field.withKey(f.name)(f))
32              )
33            )
34          )
35        )
36      })
37    .build
38
39    // a static construction of our form
40    def static() = {
41      val Config(restricted,MailConfig(host,port,user)) = Config.default
42      val noOp = (e:ReactEventI) => {}
43      val fields = Seq(
44          Field("restricted" ,restricted    ,noOp),
45          Field("host"       ,host          ,noOp),
46          Field("user"       ,user          ,noOp),
47          Field("port"       ,port.toString ,noOp))
48
49      form(fields)
50    }
51  }
52

Now we have a form component that'll build or inputs given a sequence of Field's.

Making it dynamic

Lets make this form actually edit the model. We'll do that by making another component which will construct the form.

1  object ConfigForm{
2    case class Props(onSubmit:Config=>Unit)
3    case class State(config:Config)
4
5    class Backend(t:BackendScope[Unit,State]){
6      def modifyRestricted(e:ReactEventI) = {
7        t.modState(
8          s=>s.copy(
9            config=s.config.copy(
10              restricted=e.currentTarget.value)))
11      }
12      def modifyHost(e:ReactEventI) = {
13        t.modState(
14          s=>s.copy(
15            config=s.config.copy(
16              mailConfig=s.config.mailConfig.copy(
17                host=e.currentTarget.value))))
18      }
19      def modifyUser(e:ReactEventI) = {
20        t.modState(
21          s=>s.copy(
22            config=s.config.copy(
23              mailConfig=s.config.mailConfig.copy(
24                user=e.currentTarget.value))))
25      }
26
27    }
28
29    val form = ReactComponentB[Unit]("config-form")
30      .initialState(State(Config.default))
31      .backend(new Backend(_))
32      .render((P,S,B) =>{
33        val Config(restricted,MailConfig(host,port,user)) = S.config
34        val Field = Form.Field
35
36        val fields = Seq(
37          Field("restricted" ,restricted ,B.modifyRestricted),
38          Field("host"       ,host       ,B.modifyHost),
39          Field("user"       ,user       ,B.modifyUser))
40
41        Form.form(fields)
42      })
43      .buildU
44
45  }
46

Those modify functions work, but man there's a lot of copying of case classes, it'd be nice if we could build functions that take care of that for us cleanly, especially as these structures grow.

Enter lenses

Lenses give us a way to cleanly and safely edit a piece of a larger immutable structure.

1  val _config     = Lens[State,ConfigItem]      (_.config)     (v => s => s.copy(config=v))
2  val _restricted = Lens[ConfigItem,String]     (_.restricted) (v => c => c.copy(restricted=v))
3  val _mail       = Lens[ConfigItem,MailConfig] (_.mailConfig) (v => c => c.copy(mailConfig=v))
4  val _host       = Lens[MailConfig,String]     (_.host)       (v => m => m.copy(host=v))
5  val _port       = Lens[MailConfig,Int]        (_.port)       (v => m => m.copy(port=v))
6  val _user       = Lens[MailConfig,String]     (_.user)       (v => m => m.copy(user=v))
7  val _password   = Lens[MailConfig,String]     (_.password)   (v => m => m.copy(password=v))
8
9  val _eventV     = Lens[ReactEventI,String]    (_.currentTarget.value)   (v => m => m.copy(password=v))
10  val _mailconfig = _config composeLens _mail
11