Let’s look at an example. Here’s an older piece of our code that leverages Dropbox’s zxcvbn library to assess the strength of a user’s password on each keystroke:
Essentially, every input element with the class
zxcvbn will have the behavior defined above. The input elements should have the
confirm, which refer to the message container and confirmation input elements, respectively. On
.assess(), we grab the password’s value, run some preliminary validations, then pass the value to the
zxcvbn function to give us a password score.
While this chunk of code is fairly simple, it has a few weaknesses in design:
1. There is no separation of concerns. The
.assess() function not only conducts the business logic of password evaluation, but also updates the view with the appropriate message. Ideally, this logic should be handled in two separate functions.
2. It’s hard to overwrite the default configuration. Notice the tight coupling between the DOM elements. The code expects the password input to have data attributes for the
confirm elements. We can’t specify the DOM elements independently of each other.
3. It doesn’t allow code reuse. If we wanted this logic on another page, we would have to copy and paste it over, or render this script like a partial. But if we did that, we would invoke a new anonymous function on each page load, rather than reuse the same function.
Let’s see how this looks using a different part of our password code, defined on the
wfApp.settings.password namespace. When a user changes their password, we validate their input and show the response from the server:
This logic is clearly separated out into discrete functions, which makes reading and testing the code easier. Notice, though, that we still have selectors such as
$('#password-form') scattered throughout the code.
There are a few ways to remedy this:
1. Update the functions to refer to
this.form instead. We already have the property
this.form that refers to
$('#password-form'), and using that prevents us from diving into the DOM more times than necessary. The value of
this.form could also be specified as an argument in a separate function. However, this means we would need to invoke that function on every page that uses this component; otherwise,
this.form could still point to the jQuery object from a different view.
2. Pass the the selector or jQuery object as a param. This would allow us to apply the functions to any element we pass in. But this may seem unnecessarily redundant, especially since we use the same set of functions to manipulate the same DOM element.
If you look carefully, you’ll see that the code actually has the beginnings of objected-oriented design. After all,
wfApp.settings.password is an object and maintains states like
this.submitUrl across its functions. However, it’s not quite object-oriented because
wfApp.settings.password is an object literal, which means there’s only one instance of it — and that’s our problem.
With just a few tweaks, we can flip this code into a more object-oriented model to address our configurability issues.
If we begin to think of our DOM elements and data more as concrete objects with set methods, then we can reorganize our code into something reusable and configurable.
Creating an Object Constructor
As most of our password code deals with view logic, let’s make a constructor for a new object called
This constructor should be the place where we create our jQuery objects. Since we want the selectors to be configurable, the function should consume a hash with the desired selectors and create the jQuery objects accordingly.
It should look something like this:
Here, we have three elements that belong to
- $el, which refers to the form element
- $inputCurrent, which refers to the input for the current password
- $inputNew, which refers to the inputs for the new password and confirmation
Now we can create actual
PasswordView objects by calling
new wfApp.settings.PasswordView() and passing in a hash of options.
It should look like this:
Because it’s a constructor function, we can create multiple
PasswordView objects each with custom selectors.
Replacing Other jQuery Selectors
In the other functions, we should now replace the instances of
$('#password-form') and the like with our new properties. For instance, we currently have this for the
Lines 6 and 7 refer to
$('#password-form input[name=new], #password-form input[name=verify]'). Let’s replace that with our new property
this.$inputNew. Also, line 10 refers to
$('#password-form input'). Let’s replace that with
With our changes, it should look like this:
Adding Methods to the Object’s Prototype
Let’s move all of the current functions to be defined on
PasswordView’s prototype. This means that all the
PasswordView instances will have methods that point to the same function on the prototype.
.submit() function should now be defined as follows:
Identifying Other Objects
We now have a concrete
PasswordView with easily configurable selectors, but are there any other objects that appear in our code? Since we are passing password-related information to the server, we can encapsulate that information into its own object, which can conveniently just call
Password. Let’s create its constructor function with properties that correspond to the form’s input fields:
What methods should
Password have? One thing that comes to mind is the validation logic. Right now,
PasswordView runs the validation, but that responsibility should belong on the model, since it’s a data-related check. So let’s move
We want the view to update the text based on the model’s validation result. So let’s pass in the model as one of the view’s option and grab its validation result on submit.
The other advantage of creating a separate
Password object is the ability for our
PasswordView object to use different
Password objects with different constraints. If we had a
Password object with different validation logic, we could easily pull that in without affecting our view logic.
With our new objects and methods defined on the prototype, our code should look like this:
We can see the advantages of this approach:
1. There is a clear separation of concerns.
Password handles the validation logic, while
PasswordView simply shows the text of the validation result.
2. It’s easy to overwrite the default configuration.
PasswordView doesn’t assume any default selectors, so we can pass in any form element of our choosing and just define it once.
3. The same code can be used across multiple views. All we have to do is create new
PasswordView objects on the pages that require these components. All we need is something like this at the bottom of our markup: