Build a simple client-side MVC app with RequireJS
What is RequireJS and why it's awesomeRequireJS is an implementation of AMD (Asynchronous Module Definition), an API for declaring modules and loading them asynchronously on the fly when they're needed. It's developed by James Burke, and it just reach the symbolic 1.0 version after 2 years of development. RequireJS can help you organize your code with modules and will manage for you the asynchronous and parallel downloads of your files. Since scripts are loaded only when needed and in parallel, it reduces the loading time of your page, which is great!
The app we'll createTo illustrate how to organize your MVC code using RequireJS, we'll create a very simple app with 2 views:
- The first view will display a list of users (represented by a
- The second view will allow you to add a user.
HTML and CSS filesHere is the HTML markup we'll use for this example: The navigation in our app will be the links of the
navmenu which will remain present on each page of the app, and all the magic of the MVC application will happen in the
#appdiv. We also included RequireJS (which you can grab here) at the bottom of the
body, and you can notice a special attribute on the
data-main="js/main". The value passed to this attribute is used by RequireJS as the entry point of the entire application. Let's also add just a little bit of basic styling:
MyMathmodule in our
main.jsfile: Public methods are declared using the object literal notation, which is not very convenient. You can alternatively use the Revealing Module Pattern, which returns private attributes and methods: I will be using the Revealing Module Pattern in the rest of this article.
MyMath.jsfile to define our
MyMathmodule in the same folder as
main.js: Instead of declaring a variable, we just put the module as a parameter of the
definefunction. This function is provided by RequireJS and will make our module accessible from the outside.
Requiring a module from the main fileLet's go back to our
main.jsfile. RequireJS provides a second function called
requirethat we're going to use to call our
MyMathmodule. Here is what our
main.jsnow looks like: The call to
MyMathis now wrapped in the
requirefunction, which takes 2 parameters:
- An array of modules we want to load, declared with their path relative to the entry point (remember the
data-mainattribute in the HTML) and without the
- A function to call once these dependencies are loaded. The modules will be passed as parameters of this function so you can simply name those parameters with the same modules names.
The MVC structure
The Model: User.jsIn this example, a
Userwill be a simple class with a
nameattribute: If we now come back to our
main.jsfile, we can declare the dependency to
Userin the require method, and manually create a set of users for the purpose of this example: We then serialize in JSON the users array and store it in the HTML5 local storage to make it accessible just like a database:
Note: JSON serialization with
stringify and deserialization with
parse need a polyfill to work in IE7 and inferiors. You should use Douglas Crockford's
json2.js from his Github repository for this.
Displaying the users listIt's time to display those users in the app interface! We'll have to work with
ListView.jsto do that. Those 2 components are obviously related, and they'll be linked somehow. There are many ways we can do that, and to keep this example simple, here is what I suggest: The
ListViewwill have a
rendermethod and our
ListControllerwill simply get the users from the local storage and call
rendermethod by passing the users as a parameter. So obviously,
ListViewas a dependency. Just like with
require, you can pass an array of dependencies to
defineif the module relies on other modules. Let's also create a
startmethod (or any other name that makes sense to you - like
main), to put the main behavior of the controller in it: Here we deserialize the users from the local storage and pass it to
renderas an object. Now, all we have to do is implementing a
ListView.js: This method simply loops on our users to concatenate them in an HTML string we inject in the
ListControllermodule. To do that let's declare it as a dependency of
main.jsfile, and call
ListController.start(): You can now refresh your page to get this wonderful list of users: Woooaaaah, it works! Congratulations if you coded this too!
Note: For the moment, we can only manually declare the controller we want to run since we don't have any routing system yet. But we'll create a very simple one pretty soon, be patient!
Adding a userWe now want to be able to add users to our list. We'll display a simple text input and a button, with an event handler when the button is clicked to add the user to the local storage. Let's start with
AddController, just like we did in the previous section. This file will be pretty simple since we don't have any parameter to give to the view. Here is
AddController.js: And its corresponding view: You can now declare
AddControlleras a dependency in your main file and call its
startmethod to successfully get the expected view: But since we don't have any event bind on the button yet, this view is not very useful... Let's work on that. I have a question for you: Where should we put the event logic for this event? In the view? In the controller? If we put it in the view it would be the right place to add the events listeners, but putting the business logic in a view would be a very bad practice. Putting the event logic in the controller seems to be a better idea, even if it's not perfect because we don't want to see any div's ID in there, which belong to the view.
Note: The best way to go would be having event listeners in the view, which would call business logic methods located in the controller or in a new dedicated events module. This is really easy to do but it would make this example more complex and I don't want you to be lost. Feel free to try doing it for practicing!As I said, let's put all the event logic in the controller. We can create a
AddControllerand call it after the view has finished rendering the HTML: In
bindEvents, we simply add an event listener for clicks on the
#addbutton (feel free to use your own function to deal with IE's
attachEvent- or just use jQuery). When the button is clicked, we get the users string from the local storage, deserialize it to get the array, push a new user with the name contained in the
#user-nameinput field, and put the updated users array back to the local storage. After that we finally
ListControllerto execute its
startmethod so we can see the result: Brilliant! It's time for you to take a break, you've done a very good job if you're still doing this example with me. You deserve a cup of coffee before we continue!
Note: Firefox, Chrome and Opera also have a pretty good support of HTML5 history management (pushState, popState, replaceState), which saves you from dealing with hashes.
Browsers compatibility and functioningManaging history and hash navigation can be very painful if you need a good compatibility with old browsers. Depending on the browsers you aim to support here are different solution you can consider:
- Only the most advanced browsers: HTML5 history management,
- Most of the current browsers: HTML5 hashchange event,
- Old browsers: Simple manual monitoring of hash changes,
- Very old browsers: Manual monitoring + iframe hack.
Note: A jQuery plugin is also available to manage this for you.
The routes and the main routing loopLet's create a
Router.jsfile next to
main.jsto manage the routing logic. In this file we need a way to declare our routes and the default one if none is specified in the URL. We can for instance use a simple array of route objects that contain a
hashand the corresponding
controllerwe want to load. We also need a
defaultRouteif no hash is present in the URL: When
startRoutingwill be called, it will set the default hash value in the URL, and will start a repetition of calls to
hashCheckthat we haven't implemented yet. The
currentHashvariable will be used to store the current value of the hash if a change is detected.
Check for hash changesAnd here is
hashCheck, the function called every 100 milliseconds:
hashChecksimply checks if the hash has changed compared to the
currentHash, and if it matches one of the routes, calls
loadControllerwith the corresponding controller name.
Loading the right controllerFinally,
loadControllerjust performs a call to
requireto load the controller's module and execute its
startfunction: So the final
Router.jsfile looks like this:
Using the new routing systemNow all we have to do is to
requirethis module from our main file and call
startRouting: If we want to navigate in our app from a controller to another, we can just replace the current
window.hashwith the new controller's hash route. In our case we still manually load the
AddControllerinstead of using our new routing system: Let's replace those 3 lines with a simple hash update: And this is it! Our app has now a functional routing system! You can navigate from a view to another, come back, do whatever you want with the hash in the URL, it will keep loading the right controller if it matches the routes declared in the router. Sweet!
ConclusionYou can be proud of yourself, you built an entire MVC app without any framework! We just used RequireJS to link our files together, which is really the only mandatory tool to build something modular. So now what are the next steps? Well if you liked the minimalist Do It Yourself approach I had in this tutorial, you can enrich our little framework with new features as your app grows and has new technical needs. Here are some ideas of potential interesting next steps:
- Integrate a templating system,
- Create a small external library to put all what is not purely related to your app (like the routing system) to be able to reuse it on other projects,
- Define what a Model, a View and a Controller are with objects in this library,
- Create a new abstraction layer to deal with getting the data from various sources (RESTful APIs, localStorage, IndexedDB...).