Building Delicious Forms with Ember.js

Ben Holmes speaking at Ember London in January, 2017
991Views
 
Great talks, fired to your inbox 👌
No junk, no spam, just great talks. Unsubscribe any time.

About this talk

Often when building a form we are happy once the models are hooked up and our data is being saved. In practice, a good data entry experience has many more considerations that are easily met with the Ember.js ecosystem. In this talk, Ben Holmes builds a set of example code for form UI that uses ember-concurrency, validations and error handling.


Transcript


I've just moved back from living in San Francisco for about two years, and I worked there for a company called Zesty. We do food delivery. A lot of the software we write is solving the kind of problems that Sentient should be solving for us, lots of scheduling, and lots of all that kind of thing. So, maybe we can make some people redundant soon. Oh yeah, slides. Okay, yeah. So, I want to talk about web forms, so not so much a programming talk, but maybe an engineering talk, or a talk about something that I found a lot of the people on my team miss out on. We don't have a lot of guidance from designers, or we kind of have to work things out for ourselves when we build out a feature. It's the engineer's job on our team to think about the user, so that it's like particularly difficult. The reason why I'm talking about web forms is because I always find it really difficult to make a form work. I think that I am quite good at doing it, and I still, all the time, produce bad stuff. It's like doing Ember upgrades. It's like you think you can do it, but every time you try, it's like, "Ah." So, I think in reality, we all make really terrible forms, but I'm excited about it, or I feel a bit less scared because I think that in Ember, it's a bit easier than usual to build a web form, and try to remember the dark days of when I would be trying to bind lots of different pieces of data all in like...for another JavaScript, it's very difficult. So, we make a ton of different types of forms, and actually a lot of the kind of software that we're building is data capture, managing data of some form. So, we have a weird variety of internal users. Most of the time I build anything, it's for somebody who sits a few desks away from me, so they can come and scream at me. So, a lot of them are super users, using these forms every day of their life, like four hours a day of their life. So, you need to make that experience really good. Sometimes, it's somebody who is actually a cook, and they don't want to deal with their laptop or deal with any software at all. Basically, what I want to highlight is that all of the users have a different story, and it's really difficult to build the feature if you haven't actually thought of, "What is the story?" Not just of the person coming to sit down at the computer, but if you were to talk about their experience using your website, as a story with a beginning, a middle, and an end, what is the story of that form? Because sometimes it's pretty horrible. So, I'm kind of surprised. We have a lot of good engineers. I was thinking about it before the talk, a third of our front-end engineers are Ember core team members, you think would be good at this. We do mobile forms as well. But we're really not, so how does this happen? We can make them look pretty, and it's still a massive problem for us, trying to build forms that work. So, if somebody asks me to write a form, the bare minimum is getting Ember data all hooked up, right? That's what I do, and then every now and again, I'll write an acceptance test, that's like "maybe," right? And I feel okay once I've done those things, but...oh, actually, let's go through an example of this. So, I had a feature, for example, where we've had a delivery happens, and the person making the delivery needs to say, "Oh actually, you scheduled me for this much time, but I need you to pay me a bit more because something happened on the job." Well, before this feature, there was something that was done by SMS, now we have this really horrible form that I made, to be like, "Oh, hey, pick the driver that delivered the stuff, find the residence and the job, and tell me what were the actual times that the job took so that we can pay them the right amount of money." So, this is a very typical example of how I build stuff, and that would be a form experience that I have curated. The form really sucks. There's a lot of things I haven't really thought about. It doesn't...like that button, I just pressed it, right? I don't know what it did, I don't know what happened. So, the form doesn't tell you when something goes wrong with the API, when there's something wrong that we messed up with, right? It also doesn't help the user if they mess up. There was those time inputs that I wrote in, what?12:45 a.m.? What happens if I used a dot instead of a colon? So, I don't really know how the server is interpreting the data that I've entered. Sometimes that's more nuanced than that, but sometimes it's just basic text entry, but in the case of that time entry, it's not very obvious what's going on there, and it doesn't tell the user, like, what's their progress through the experience of that form. So, when I pressed that button, for example, it wasn't like, "Okay, we're thinking about this. Give us a moment. It's all going well." So at the very least, I would want the submit button to give me that information, but I think a really good form experience helps you out the whole way through the form and be like, "Yeah, everything's still okay. You're good. You're good. You're good." I always forget to do these things, and really if somebody is going to make a poor request to me with a form in it, I want to make sure that they've addressed all of these points. So, the way that I think Ember is just... I don't know about "really shines," that's maybe an overstatement, but the things that I found particularly great are: it's really easy to set up data bindings, has a great ecosystem of components and libraries, so a lot of the kind of complex form interactions that you would normally have to build yourself, you can just get. And also a lot of the more core tools, like ember-concurrency and Ember Data, also provide us with a lot of support for the kind of work that we're trying to do when building a good form experience. So, the first point that I made should be like one of the easiest things to do, but I still always forget to do it. I still never actually do it, it's like displaying some kind of error message when the API complains. This one is really easy. We're going to start with the easy stuff and move on to more complex stuff. The save method, or any kind of thing that you do, say you're using Ember Data, it gives you a promise. So, you might as well take advantage of that whenever you're doing something like any kind of interaction with the server, think about putting a catch block on it. And then taking this error message that is returned and either thinking about it a bit, or just give it to the user. Do something to show them that that save didn't work, because you think your code is infallible, you write it, it's always going to work. There's going to be something that goes wrong. So, that's normally the first thing, people come back to me and they're like, "Oh, I submitted the form but I didn't know if it actually saved." So, I go back to the index route, and see if it's there and then go back to the form, and then I've trained all of my users to do this really weird navigation story. It's horrible. Ember-data actually has an errors object built into it, so if you're using a standard pattern, say, for example, active model adapter, so the standard way of producing an API when you're using rails, Ember Data will consume that, and give you an errors object on your model. So, if you're using Ember Data, you've got these errors object for free. So, that's really handy, and you can use it then to produce some kind of an error message like this. So, that still requires some kind of a round trip to the server, it comes back, you annotate your input. I think that's still a really bad UI. Because what I've found is that people are only thinking about the stuff that they're inputting as they're inputting it. So, if you've got, especially if it's a really big form. We've got some very big forms. If you put a number in, and you get the response back and it's like, "Oh, sorry, this thing way back up here was not correct." It's so much better to tell them that it's wrong immediately, and because we've got data bindings that are very easy to set up, it's really easy to specify, "Oh, look here, this reason field that I didn't fill in right here is actually required." So, we'll say, "Oh, I left my keys in the car." Oh, okay, it's too long. So, that kind of instantaneous response is, in my opinion, massive. The other benefit that data bindings provide is that we can give the user instantaneous feedback about how their data is being interpreted. So, in the example of my start time and end time, if I type in the number 9, it's potentially bad input, but I know that the server is going to get 9 a.m. out of that, right? I do like 12:49, sure. So, this is a much more natural way of interacting. If I've got a power user inputting data, they don't want to have to use drop-downs to do like, "Oh, it's 12, and then 45." But I need to give them some kind of feedback to make sure that I've understood what they're typing correctly. So, the other benefit is that you can actually add some more kind of context to the data that they're entering. So, as an extension of that example, we could take that entire form that I presented earlier and give a preview pane, like a preview document that you're building as you fill out the form. This is all really easy to do with data bindings. So, this now looks more like a document that I'm filling out that's like, whatever it is, going to get processed, and I can add annotations about like, "Oh, this bit is invalid." By actually annotating the document. There's a lot of things I want to play around with this. I want it to actually get to the point where you could click, for example, on the preview, the field in the preview that needs changing, and then that will present you the input that you need there. So, that's where I think data binding is super useful. The other problem that I frequently face when working with any kind of form data in Ember land is that we often think that we have two data stores that we're worried about, the server and the data that's in the front-end, say like Ember Data. We really have three, and maybe four, depending on how you look it up. The stuff that's in Ember Data is fundamentally global to the entire app. So, if you do like model.sat, or property, in your form, even if you haven't pressed the save button, it's going to be present in the rest of your app. Say you've got something on the index page, your user navigates that page. It's like the data change appears to have been made when it actually hasn't. So, you end up with this separate state, that is the state of all the variables that you set in your controller. You also have the state of the actual text inputs in the dom, so you've got four states to worry about. So, if you call model.save, it will take the stuff that's in Ember Data and put that up to the server, right? You call model.sat, that's going to take the stuff that's in your controller in the dom, and put that into Ember Data. And fundamentally, we want a way to be able to edit information in your form without editing that information across the rest of your app. So, there's a few ways you could do this. You could actually just take something, and put it just as an attribute directly on the controller. So, say, for example, I could have a generic field, so like an action on an input, so every time I put anything into that input, it caused this set reason action which stores that on the controller, and then I can worry about doing the save later. I think that's a bit...it's something that I use a lot, but you can...you know, when you start typing that a lot, you get that pattern occurring across all your different forms or your different apps. What you could then do, especially if the form is more complicated, is create some kind of a regular JavaScript object, like a request form, right? Initialize that when the page loads, and then you're actually editing directly the fields on that request form, and then when you want to do the save, you sync them up with the Ember Data model. Again, I think this is a bit too custom for me. There's this add-on out there called ember-changeset, there's actually a few out there, but I really like this one, which wraps up that very custom behavior into a generic way of managing a particular set of data that you're editing in the form. And then you give it validation concerns, and then when you hit the save button, it only puts through to Ember Data things that are valid. So, in this example, I've got a form here with a changeset, and I can change the text in the form and changeset.reason is being changed, but the actual stuff on Ember Data has not, until I hit Execute. And then if it's not valid, say like the reason is required, and then I try and hit Execute, it doesn't happen, right? So, that save method depends on the validations that you've specified to ember-changeset. So that gives me a really standard way in which I can say, "Hey, all the engineers, you're going to do it this way," and then everybody has a familiar pattern in how we address any kind of form behavior, instead of having just this random behavior that everybody specifies in a very custom way for each form. We have a standard way of defining all of the validation for this extra time request in a single file. So, the way the ember-changeset allows you to specify your validations is that it gives you this validate function, where you have...it's going to give you, "Here's the key that changed, here's the old value, here's the new value," and asks you, "Are we still valid? Is this valid? What do you think?" So it gives you a generic way of dealing with this. It also gives you some high-level ways of implementing these methods, so you get a validate presence method, a validate length method, but then you can still implement your own custom ones. So, for me, this has been massive. Initially, I was very skeptical that it would work in the way that I need it to, but ember-changeset has been massive for me. The other aspect that we haven't addressed in the form is showing the user the progress of the request, or the progress of the form being completed. In this example, I've got the Submit buttons showing you the status of the request that's happening there, so say, for example, I can click on the Submit button, and if the request works, that's great. If it doesn't, I want to see that visually, and that's something that ember-concurrency has been really great for. You can get directly in your handlebars file an idea of what is the state of this task. So I've got the save task, is it executing right now? We have actually just completely removed all use of actions and replaced it with ember-concurrency tasks, because they have that added flexibility that has been very useful. So, for example, it allows us to set up an interface that involves autosaving, right? So, the trouble with autosaving is what you don't want to do. I've got... There's a back-end engineer who, he doesn't know very much about Ember at all, but what he does know is that...he's like, "Oh, Ember, that's that thing that creates loads of post-requests, right?" Because we've got some instances of production autosaving, and it just bombards the server with stuff, and what you can do is introduce...what you really need to do is introduce some kind of a debounce function that waits until everything's settled down a bit before it makes that commit. So, if I'm editing this field, it's going to wait for a whole second before it starts doing that save, right? So, it's not hitting a save every time I make a keystroke, but it's performing the save function every time I hit a keystroke, and the reason why this is working is because ember-concurrency has given me this method here, which is I'm going to yield this step, and ember-concurrency waits until whatever I'm yielding has executed. So, in this case, it's the timeout, and then I said the method is restartable. So any time that I call that method again, it cancels all the other instances of it running, and says, "Forget them, we're going to start this new instance," and so it has...the timeout has to complete, basically, for the save to happen. So, personal words of warning, if you're autosaving content, put in a Save button even if it doesn't do anything. Everybody gets really scared when there isn't something to click. So, for example, there was a form that I built where the "X" button was essentially the Save button. It was like, "Close down the form, we're done." Because everything was already saved, and to me, that was intuitive. To the user, it was like, "Oh my God, what's going to happen to my data?" And like I say, anybody working on back-end might hate you a little bit, because, I don't know, we've got this rails monolith. They can't handle anything as fast as we can. One of the things in particular about building things in Ember, is that it allows me to get creative. It allows me to build fancy stuff. I actually, instead of using any particular presentation stuff where I just built the presentation code in Ember, and we've actually ended up using the code that powers these slides to power a form, where we've got the questions of the form on the left-hand side, and then each of the actual inputs, the questions that it's asking, is like a slide, right? So, this allows me to create some really bad patterns very easily. So, in this example, we might want to make a component that is like a workflow form, it's what I ended up doing for the extra time request page, because the extra time request page requires earlier sections of the form to be complete. I don't want them to be visible, so I've got these set out in components, and then I can tell the section of the form...I've given it a way to say, "Hey, I'm done. Everything's valid, we're good." So, this section is complete, and so let's make the next section visible. So, this was really easy to prototype, and it meant that I was able to make a UI that looked a bit more like this. So, you have a very clear idea of where you are in the process of filling out that form, and instead of it just being this great big wall of inputs that you need to fill in, it's like, "Hey, I'm going to ask you a question, you're going to give me an answer, and then we move on from there." So, I find this way of interacting with the form a lot better. I mean, I try to take a lot of cues from how a lot of mobile interface works. So I find, often, we end up filling up the page with content, with input fields and everything, like we make the form two screen lengths big. We really don't need to. It's like great power, great responsibility, right? So, these are things that I'm personally really bad at, so I suggest looking out for it, I don't know if you guys suffer from the same problem. Controller and component lifecycles are very different, so if you're putting form data into either of those things, watch out, because the controller lives forever. So, if a user navigates away, and then comes back, you're not going to get any kind of tear-down, and the data will still be there in the forms, so they might get confused by that. The other thing is that I haven't come up with a good way to do keyboard listening. You might have noticed I made some really inaccessible forms, because they didn't allow me to do the left and right key. Left and right key is doing the slide navigation, so there's supposed to be some stuff for this in ember-concurrency, but I still haven't worked out how to do it. I think there's a lot of situations where you want a keyboard listener to be specific to a page, but you want it to be torn down when somebody navigates away from the page, right? So, if anybody has any advice about how to do that, I'm very interested in hearing from you, but I still haven't worked that out. So, thank you.