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