About this talk
What would a form look like, if we were to create something for ourselves? It would have to have convention, over configuration. It has to provide minimal fuss, but also complete flexibility. Watch how Will managed to do that.
Today, I wanted to talk about forms. It's not that exciting, really. But, it is something, that we tend to have to work with a lot, as web developers. And so, I think it's quite important, especially if you have an application, that relies extremely, heavily, on forms. If you have two new developers, or even like yourselves, as senior, or intermediate, or whatever level you're at, you kind of don't wanna spend a long time, having to repeat the same thing, with forms. So typically, an Apple has some sort of style guide. You'll have to apply the same classes. you'll have to kind of redo these defaults, every time, if you're sticking to this typical HTML form structure. And so, I think, maybe a year ago, myself and one of our other founding developers, we got to together, and kind of sat down, and thought, okay, what would a form look like, if we were to create something, for ourselves? And we decided, that it had to have convention, over configuration, to kinda stick with the Ember ethos. And so, we try and provide things, like same defaults, and make decisions, that we thought would be best, for our application, and for our developers. It had to, really, what it boiled down to, is it had to provide minimal fuss, but also complete flexibility. So, we had to provide these same defaults, but allow any developer, at any point in time, to jump in, and do things their way, right? Because, one solution doesn't always fit all, especially when it comes to things, like forms. And it kinda had to be like, at its base, at its very basic implementation, pretty simple. Easy to understand, had to follow things, like normal HTML conventions, when it came to things, like events, and other properties, that you would detach to these. Yeah, so you start with something like this, which is a fairly simple form, and this is a form, taken from our application. And this is actually using the... Solution, that we came up with. But, this isn't actually very difficult, right? This is one of the most simplest forms, in our application. We have a text area. We have some arrows here, because the address can't be blank. We have a selector, which is a country, US States, quantities, which is a number field, and then another message text area, down at bottom, and then two buttons, save and close. So, it is a very simple form. But then, you got, also, quite complex forms. So, this is another form that we have, which is probably one of the most complex forms in our application. I would say, second most complex. But, we have a selector, which searches every user, that works here, at the company. We have a client contact, which belongs to a different model. We have other select boxes. We have this duration field, client availability, a whole bunch of things going on. And to add to that, there's a lot of stuff, that's going on, in the background, right? So, we have a whole date picker over here, that has... It yields each day, and then each day can have different properties attached to that. This field is actually displayed, in minutes, but it's stored, in seconds. Here, we have time zones, which is a whole world unto its own. But here, we're converting, this is a conversion of that time zone, against this time zone, and then we display that output there. We have actions. We have radio buttons. We have combined fields, down at bottom here. So, it's like very complex form. And this is the exact type of thing, that we wanted to sell out, to solve. We wanted to say, how can we say, how can we build something, that takes something, like a very simple form, all the way through, to something, that's a little bit more complex, and still carry the same conventions? Anyway, it was a little bit challenging, but I think we, what we ended up coming up with, I don't think it is unique, I think it's been done before, with other forms. Though, I think some aspects of it, are pretty interesting, and quite unique. So yeah, what we wanted to have, was field errors, labels. We wanted to be able to disable fields. We want to be able to disable the whole form, if we wanted to. We wanted consistent styles, so that the developer never has to think about, if they have an input, they shouldn't have to think like, what class do I need to attach to this input, to get it to look like the 50 million other inputs in my app? It should be easy for testing. If we're building our own form, then we should take liberty, right, and we should add things, that are gonna make testing easier for us. And then, we can build helpers on top of that as well. And it should also be really adaptable. So, it should be able to support future field types. So, it should be very pluggable. And we're always thinking, right? We always wanted to get to this minimal fuss, but complete flexibility. That was the aim, over everything. And yeah, it was like a bit of a challenge, for Andre and I. And really, there are two things, that you need to kind of, to grasp, in order to understand how the form works. The first one, is contextual components. Is everyone familiar with contextual components? Okay, cool. That should be easy. And then, the second one, is this concept of proxy objects, or change sets, if you've used Ember Changeset. Are people familiar with these as well? Okay. I'm gonna go over them anyway. But, what are contextual components? So, I think it was a year and a half ago, maybe a little bit longer, maybe a little bit shorter, give or take, a few months. But, you previously, if you wanted to have this concept of a nested component, you couldn't really do it. So, you could name space them, but you couldn't have a concept, of this component, or in this instance, you have my-component, you pass in a user. It might need some knowledge about that user. Then, you have a label, an input error, select, and you couldn't have this concept of, well these label, or this label component, is actually a child of my-component, right? There's an inherent relationship there. And that could only be defined, by the name spacing, or by the structure of your application, in the folders. And also, you have a bunch of repetitiveness here. So, you have the user, and then, because there's no shared context here, you have to pass the user into each one. And I think a lot of us would probably have situations like this, in some of our apps. I know we do, with some widgets, and weird things like that, where they've got headers, and then content, and then footers. But, this was a pretty typical scenario. But obviously, it's not very good. It's not ideal, there's a lot of repetitiveness. It's not very clear, it's not very easy to read, and understand. So, the solution they came up with, was this idea of a component helper. So, we're all familiar with helpers. I have wide space there, and that's driving me nuts. But, you have this concept of the hash helper. People familiar with the hash helper? Essentially, it will take these, this is actually wrong... But, that's okay. We'll roll with it. But essentially, we'll yield this hash, and what you would have is, you would say, label equals component, and then you would have input equals component, errors equals component. And you can kind of think of that, as like a normal object, and the key will be label, and the value would be this component, which is not actually evaluated, yet. It's not evaluated, 'til you call on that argument, that's yielding out, into that ash. And so, this is basically the essence of contextual components. Because now, I no longer need to, as a developer, not as an add-on implementer. So, this is what you would have, as an add-on implementer, and you still have this concept, of repetition here. But, I think that's okay. That's okay, because as an add-on developer, you're designing an API. You're not reusing this, in multiple areas of your application. But, as a developer, you just want the benefits of this, and that's what this helper's all about. That's what this contextual component is all about. It's designed, to make it easier for add-on developers, and for developers in your team, to build components, that expose some level of an API, to make the job easier, of building your application. So yeah, you can see here, that I pass in user, to my top level, so this is my-component.js, and I pass in disabled here. And then, I can pass this into each one. And then, essentially, what that's going to look like, we'll look at, in a bit. But then, it changes the thing here, quite dramatically, because I can yield, I get an argument back, called component, or something. I can name it whatever I want. And then, all I need to do, is component.label, and that's it. I don't need to do the disabled, or the user anymore, because that's all getting passed in, through the my-component component. Okay, so that's contextual components. What are proxy objects? I'm sure, in some of our applications, or perhaps, like earlier on, in the days of Ember, this would've been a pretty typical scenario. So, you would've had your form. It would've had first name, last name, email, and that was probably directly two-way bound, to your model, or some form of that. Perhaps, like an alias, on your controller, or your component. But, this was a pretty typical scenario. But, this is not ideal, because the Ember store is global. And so, that state, when you mutate it, is changed everywhere, in your application. And then, you, as a developer, you're now, you've from, trying to update first name, to having, to go back, and roll that global state. And that just shouldn't be a concern, that we have to worry about. And so, there's this concept, of a proxy object. I think in Ember, it's Ember.object proxy, or changesets. So, these are kinda similar things. Changeset is a similar concept. But essentially, what you're doing, is you're populating your proxy object, using defaults from the content. And then, when you change these, you're only changing them on this proxy object, or this changeset. And only, when that changeset, or this middle person here, is valid, or in a state, that you're happy with, do you then persist that back. And the advantage of this, is that if my user decides to fill in this form, and then cancel, because this is all within a component, I don't have to do any clean up. I just have to throw it all away, and forget that the thing ever happened. I've not mutated any global state here. All I've done, is create an object, populate it with some defaults, and then throw it away. So, that's kinda the concept of a proxy object, or changesets. So yeah, and these give us really good things, though, things that don't necessarily... That easy to use, on an Ember model, because these can be just plain POJOs. These don't have to be anything special. And so, because of that, we can create our own conventions around these. So, we can do validations, we can have temporary properties, like password, and then password confirmation, and then we can have something in there, like a function, that compares them, to make sure they're correct. We don't have needless store creation. So, if I want to create a new record, I don't have to create that, until the object, that I'm actually mutating, this proxy object, is in a valid state, and can be persisted back. And I also avoid two way binding, and I stick to this principle of actions, actions up, data down, or data down, actions up. One of the two. Okay, so back to forms. So, this is really, a kind of brief look, of how we do the forms. There's a lot more in here, that I would love to cover, but honestly, I could keep going forever, so I kinda have to scope myself. But essentially, this is the X form component, okay? So, if you think about my-component before, this is the component, that we have generated, the form component. Now, it's called X form... Because, naming things is hard. And so here, we have just a plain component. And what this does, is it yields out a few different contextual components. This is an exhaustive list. I'd cut this down a little bit, to kinda simplify it. But, we have a text area. Actually, I should start at the start. The first thing you get passed in, is a changeset, and then a global disabled fault, sorry, global disabled state. And the reason for this, is because, if you imagine that your application uses models, or that go to a page, and then you're required to load five other different models, like in the case of that scheduling form that we had before, you don't want your users to be able to interact with this form yet, right, because it's not ready. And so, we have the option here, to just pass disable in, as true, and that can be running off something, like an Ember concurrency task, and as soon as that task stops running, and is idle, the whole form will be enabled, and all that data will be in there. So, we can use Ember concurrency tasks now, to control the whole state of our form, with just one property. And because we're using contextual components, these just get passed in, to every other field, and then we have classes, which get applied, and then the X form has a special CSS file, which then makes everything look the same. So, everything can be disabled. There is a drawback here, which I'll go into later, which I'm still trying to solve. So, we yield out a hash. We have textarea, select. And under the hood, this select is really just a power select. So, you're doing that whole concept of, there are these solutions out there, that exist... Things like power select, or power calendar, that are really great, right? And we don't wanna lose those benefits, by trying to reinvent the wheel. So, what we've done here, is we yield out a select, and the underlying component there, is actually just a power select. And then, we provide defaults, even onto that power select. We have a plain old input, and then a button, and a label. Okay, so if we dive into that input component, this is what that input component looks like, and... If you imagine what a form is, and you imagine what a field, or what we call an element is, we've decided that elements are made up of three things. An element on a form, is made up of a label, a field, and an error list. And so, you have, in that first example, the label would've been name. The field will be whatever the input is, for that particular element, and then the error list is, you know, basically a list of errors. And... And so, at any point, you can use all of these, you can use none of these. If you want just the field, you don't have to have the label. You just wouldn't do form.label, or whatever. If you don't want the field, for example, you can do that. Yeah. I have an example later, where I can actually demonstrate some of this in action. So, if we dive down a little bit further, let's have a look at the field. So here, we have, this is an example, of taking a bit of liberty, as an add-on developer, or as like a developer building an API, in this init, where we define a computer property. But essentially, it's a very simple thing, right? The tag name is just input, Then, we have a few attribute bindings, for some normal HTML attributes. Value, disable, read only, placeholder, type, right? So, we have default support, for things like number, or email, or website, or anything like that. And then, we apply our class names, which is specific to this particular X form. We do things, like auto focus. So, these are things that, there is an HTML5 attribute, which is auto focus. But, that only works, if the page loads, and it's not, and it's the only input on the page, right? If you have a component, that is in a model, or something like that, auto focus doesn't always work in that instance. So here, again, we can take liberties, and we can say, if you've provided auto focus, we do some quirky jQuery stuff in here, to select it, which I removed, I think, to hide that shame. And then, we do a few more things in the init, so we define a property here, to watch the value, which I'll get into, in the example. So, each element also has a property, which reflects, and it tells that element, what field in the changeset, or proxy object, it's responsible for. Because, by default, that... This knows how to update itself. But, that doesn't mean that you can't change it, which I'll dive into, in a bit. And then, we provide the default actions as well. So, we expose those. So, we say, if you've provided, as a developer, if you've provided me an action, on input, I'm just gonna yield up, and I'm gonna give you the event, and it's up to you, what you wanna do, right? If you don't provide that action, then the default is to just set that property onto the changeset, or the proxy object, that you've passed. So, always tryin' to think, what is the same default here? What's the expected thing? But, then providing that complete flexibility, for the developer to jump in, and do whatever they want. Basically, what this slide says. Okay, so an example. So here, we have a form. I'll bring up the example controller. So here, we have a controller. The root is just example. We don't have a root. We don't have a real Ember model, either. So, you'll just have to imagine that, a little bit. But, in init, we create this model, which you typically wouldn't do, because that controller is a singleton, so this would only get loaded once, so typically, this would happen in a root, so you'll have to forgive me. But then, we set this real model, so we're basically faking the root action. You can see here, that age, right, is stored in months. So, I'm tryin' to replicate this tricky situation. It's something that's stored, but then displayed differently. Then, we have an action, which is logObjects, which you'll see, when we get to the form, and then another action there, to set the age. So, this is the form. So, this is what it looks like, in code. This is what it looks like, on the... On the page itself. First name, Will, last name, Raxworthy, age 30. And so, yeah. And so, if we walk through this form a little bit, first, we use the X form component. I've used class names here, because I didn't wanna do my own CSS, so I copied from a previous form, just to get the spacing, and stuff like that, right, to put in the middle of the page. I'm using a changeset, so we use changesets by default. But really, this could be anything. This could be a Emblem object proxy. It could be a POJO. It could be whatever you want. We just use changesets. And in response, or what gets yielded out to you there, is this form, this component, but you also get that changeset back. And this is really important, because this is what lets you have that complete flexibility, over what your form elements will do. And then, we have an input, so we do form.input. I tell it which property this element is looking after, which is name. I have a label, first name, by default. So, if I was to get rid of this, and just do this, by default, that's just going to take the property name that I have, and capitalise it, and just do name. I don't know, that seemed reasonable, when we did it. But, you can override that. You can yield that, and you can do whatever you want. You can have a label, that is like select box, now. You can have a label, that is a picture, or a gif, or something like that. You can do whatever you want. Or, you can just put first name, or you can just have the default. And then, we do the input.field, and this is this bit here. So, you can see already, right? We've created this changeset. It's backed by a real model, which was named Will, last name, Raxworthy. And just by doing this, it's given me this input, which then pre-populates my default values in there. Okay, so we had the same thing, for last name, and then we have age, again, which I'll get to in a second. And then, down here, we have... We have a button. So, we have this concept of a primary button, and then we have a concept of a secondary button, which is just like a cancel, or something like that. Oh, can everyone see that? So yeah, we've passed primary true. I'm not a big of passing primary true. If I could do it again, I would probably do button dash primary, or something. But, it's okay. And then, we have an action here, which is on quick. So, by default, a button doesn't know what to do. You have to tell it what to do. So, in this case, we're going to call logObjects. So, if we have a look at logObjects, what's it gonna do? It's going to log the real model, and then it's going to log each of the values, of the changesets, so name, last name, and age. And this is really just to prove, that... That the changeset is working, and that we're updating these values, as we want. So, if I was to change this, to... James. And then, I click this button here, you can see my original object, or my real model, hasn't been changed, in any way. But, my changeset, the values have been changed, and only the ones that... The ones, that have those values, are still the same values. And as you can see, by default, right, this is the pretty same thing. You type in the input, the input updates the changeset, and then you get the changeset back. So, where it becomes a little bit tricker, is here. So, we have this age form, and the age form, we've decided, we're gonna store it, in months, but when the user uses it, we want to show it to them... In years, because that's a better way to view your age. And so, this is an example, of how we can go completely custom on this thing. So, the default now, is when I type in... If I go back to this example, and I type in something, like 20, I want to see 20, but what I actually want to store, on the changeset, is... 240. Alright, it's like stagefright. I didn't even wanna try. So, you can see it, right? So, by default, the value here, would be changeset.age. But, we're overriding that now, and we're saying, if changeset.age is an integer, and I'll show you why we have to do this, in a set. So, integer, or integer is a helper we've made. If it's an integer, then divde changeset.age, by 12. And if it's not an integer, so it's like null, or text, just yield.text. And this is so that I can say something like, I didn't catch it, but in the scheduling form, if I was to type test, right, you don't wanna show NAN. You want to validate, and you wanna say, you've typed test, but this is, this field should be a number. I can show a better example of that. It actually works. But yeah, so we check, if it's an integer, divide it by 12. Although, I show the age. And then, on input, so rather than going with that default action, of just updating the changeset, we want to run our own action, which in this case, is if there is a value, set the age to the value, divided by 12, I'm sorry, multiplied by 12. Otherwise, don't do anything. We don't wanna do anything. So then, when I type in here, and I say something, like 50, and then I hit save, you can see, the changeset is stored at 600, but the value of it, that is displayed in my input, is still 50. So, my user has no idea, that what they're typing in, is stored in months. But, as a developer, I have full control, to show that form, exactly as I want. But, I still get all the other benefits right. So, I still get all the classes. I still get all the... All those same other defaults. I get the error lists, and everything like that. And if we look at the classes, we can see that we have, we generate all of this stuff, which is useful, for testing. So, we have X form field, dash, dash name. So, we use those properties, because you've told us, what that element is, and so we can use that, to build class names, which in your Q unit test, you can now just go projectform.xformfill dash, dash name, check the value of that, and then off you go. You kinda don't need to traverse this hold on down, to input. So yeah, we can take a lot of liberties here, and we can provide a really good experience for someone, who's building a form. So yeah, like I said, there's a lot more, that I can probably talk about here. So, you know, things like select boxes, some more complex examples of how you would structure this, how you might go about testing it, some other helpers that we've written, to kinda make this stuff a little bit easier. I think I'm running out of time. Does anyone have any questions, at all? - Yes, I... The whole thing itself, isn't open sourced, purely because originally, it was changing so often, as we kinda figured it out. I would love to open source it. I think, there's another one, called Ember form four... Which is quite similar. Which, also handles things, like changesets, and stuff like that. It also does translations. Yeah, I would love to open source it. I think, before that, there's things like accessibility, that I've also liked to figure out, like how you would go about trading defaults, for accessibility. Yeah, things like that. The other gotcha is, so here, I can say, I can disable an individual field. So, I can say, if this value is blank, then disable this particular field. So, if I go back to my example template, I can say, here, disable equals true. And then, if I go back here... So, age is now disabled, and you can see, this is an example, of how I've given up one property, but it's changed everything about that field. But now, if my form, as a whole, is dependent on a running task, I've overridden the default here. So, I then, have to repeat this, if this task is running, disable this field as well, and this field. So, there's a few things, like that, that I'd like to figure out. So, how would you actually merge these disabled states, so that you can still disable the whole form, but then have the ability to, have kinda nested disabled states. Another thing you can do is, you can provide modifiers, so if you have instances, where you want to style a field, slightly differently, you can provide a modifier, and that will, if you're familiar with BEM, it'll be Xformfield dash, dash, name, dash, dash, somethin' or other. And then, you can style that, specific to your situation. So yeah, there's a tonne of things, that I'd probably love to change about it, before then. Any other questions? Okay, cool. Thank you very much.