Intro Rant
I've been using Aurelia since it was first available to the public. I have made a lot of bad decisions with it and a lot of good ones over the course of the last year or so. One project that I embarked on had a large amount of forms that needed to get filled out, validated, and sent to the server that expected the data to be in a particular format like any other API call.
I was using mongoose on my back end as it was Node.js powered with mongo as a storage solution. I really wanted to find a way to reduce the the overhead of form validation and value selection. So I went to take a look at mongoose for the browser, nothing extreme just a way to bind the validation I already had in my schemas for the back end to the forms on the front end.
After finding that to be a relatively easy task I then had to find a way to get my schemas to load into the browser and into my node modules. I first tried some old crusty methods of wrapping the returns in various statements depending on if env variables could be found to determine if I was in Node land or Browser land, but ultimately it became way to bloated to be included in every schema that I wrote.
So I moved on to a cleaner solution of browser first. Since I was using Aurelia and Babel to handle keeping things clean and the live transpiling I could just write straight ES6 style schemas. which I could then load using Babel on the backend as well. thus began the journey of my mongoose schema input custom attribute in Aurelia.
Building The Custom Attribute
I normally start things out by just defining the logic-less parts of a new component that I am working on. This was no different, I created a empty custom attribute using Aurelia bringing in all the relevant modules I'd need to make it function. it looked something like this
This obviously is not very exciting in and of itself; however it shows some key interactions with the Aurelia component life cycle. These life cycle methods on the class get called by Aurelia when our component is constructed/deconstructed and attached/detached to the DOM, this is bind and unbind respectively in our class definition.
Next I wanted to identify some issues that I've come across with some other form utilities and see if I could address them with my own implementation. The list ended up being relatively short but crucial to reusable form validation components
- Two way binding for values in my mongoose model
- error class appending/removal on state change
- Independent logic set that lives outside of any view model
- Validation logic throttling
- onChange event support for things such as dropdown menus, multi select, and radio buttons.
- Callback support for when something is validated
- No jQuery, only core JS.
After I finished my list I didn't feel like it was something that couldn't be done or was to wild to keep in check without bloating it. With this list in my mind I added properties to my class definition to represent my list items. In the end it looked like this :
Most of these properties are pretty self explanatory with the comments provided in the code sample. Most of these are just properties that get bound from the parent view model so that the attribute has access to the memory reference so that two way binding can be used. It is worth mentioning that we explicitly make the binding type of the component two way, this is to enforce the behavior between the view model and the attribute references.
Building The Attribute Functionality
So now we have a pretty solid skeleton for the functionality that we can load into a view model and use as an attribute on a input field. Sadly so far it doesn't really do anything; the functionality still needs some scope as to how we bind it to the view and the view model. Lets go over how we want the HTML and ViewModel to look like when using this custom attribute.
HTML Bindings
The HTML binding part of the custom component is relatively easy and simply binds values from a view model to the reference containing properties of the custom attribute instance. Given the properties we made on the attribute the bindings can look like this
ViewModel Bindings
Given the attributes in the HTML template and the properties of the attribute we can create a sample view model to hook everything up for examples purpose. We need a small mongoose model, a callback, and the path name of the field to validate in the mongoose model. In the end we can use something like the following
We really don't need much in the view model here, just a callback and a mongoose model. Normally I would suggest creating your schemas separately and importing them; but for the purposes of this example we will define the schema inside the view model.
The callback that you pass to the validator attribute gets passed either an object containing the path that was validated if it was successful or the error object that the mongoose validator returns for that paths validation call.
Writing The Attribute Methods
Constructor
Our constructor does not do a lot of logic here, we simply assign the element handle to a property on the class instance that gets passed to it by the Aurelia injection/creation life cycle. We also create and grab a logging instance from the Aurelia framework.
Bind
The Bind method is invoked by the Aurelia life cycle when the attribute is bound to the DOM and the view model of the outer template instance that the attribute was required in. The Bind method is passed the context of the parent view model which allows us to invoke methods that are on the parents scope ( like we do with the callback that you pass to the validator ).
A word of caution : It's tempting to use this context for direct bindings to the parent scope instead of doing the binding on the template/DOM level ( What I personally call Angular Scope Hell Syndrome ). This really is not the best way to do this and creates very tightly bound component that relies on things to exist in every view model that acts as the parent context to the custom attribute which makes it very brittle and not reusable.
Unbind
Unbind is invoked in the life cycle of the attribute when the attribute is detached from the DOM and current route scope. In this case it is used to remove the the listeners that were attached in the Bind method.
validateField
This is where most of the magic happens for the custom attribute. This is where the actual validation using the mongoose model happens using the path name string and the model that we bound to the instance in the html bindings. There are a few things the method does
- Calls the mongoose document validation method
- Because of the fact that the mongoose document validates all fields whenever you call the .validate method we have to look for our current paths error specifically by using the path string.
- If we have a validation object and we find an error matching the path string that was bound to the instance then we throw a not validated error.
- If we do not have any validateResult object then all is well OR if now error was found for this path then it has passed validation.
- In either of these cases we either add or remove the error classes that we also bound in the HTML bindings.
- These error and success classes do have defaults as seen in the class property definitions but those can be changed to suite any of your needs.
Due to the fact that I didn't want to use anything but pure JS for this the validateField method seems a bit chunky and I am sure could be reduced in size with some modifications. But this blog isn't about perfect code, it's about learning.
The validateField will apply the error or success classes to the parent of the input field. This allows you to wrap your input fields and have them show the errors based on the mongoose validation result.
valueListener
The valueListener method is what actually gets attached to the event hooks on the element that the attribute is instantiated on. ValueListener is where throttling is handled via some simple time out mechanics that aren't very mysterious. Basically all that happens here is the throttle flag is set when we start validating the field and then when another request for validation comes in we set a small timeout for the next validations to occur in sequence.
While not perfect it does work, though I do have the thought of potentially using the binding behaviors here that Aurelia comes with; I will add my findings when I get to that here.
The Results!
It's been a quick and hopefully painless ride to our custom validation using mongoose schemas on the frontend. Here are some examples of the forms that I have put together using this mongoose method. The icons on the right of the input fields change depending on the success and failure classes that get added or removed to inputs parent. I set it up this way so I could add other elements in the same element as the input which could pick up on the success or failure state of the input field; but that is a another article.
Empty
Invalid
Valid
Links