Weâve reviewed the 1970s, but let us now return to the here and now. In modern times, the MVC pattern has been applied to a diverse range of programming languages, including of most relevance to us: JavaScript. JavaScript now has a number of frameworks boasting support for MVC (or variations on it, which we refer to as the MV* family), allowing developers to easily add structure to their applications without great effort.
These frameworks include the likes of Backbone, Ember.js, and AngularJS. Given the importance of avoiding âspaghettiâ code, a term that describes code that is very difficult to read or maintain due to its lack of structure, itâs imperative that the modern JavaScript developer understand what this pattern provides. This allows us to effectively appreciate what these frameworks enable us to do differently (Figure 10-1).
We know that MVC is composed of three core components, described in the following sections.
Models manage the data for an application. They are concerned with neither the user-interface nor presentation layers but instead represent unique forms of data that an application may require. When a model changes (e.g., when it is updated), it will typically notify its observers (e.g., views, a concept we will cover shortly) that a change has occurred so that they may react accordingly.
To understand models further, let us imagine we have a JavaScript photo gallery application. In a photo gallery, the concept of a photo would merit its own model, as it represents a unique kind of domain-specific data. Such a model may contain related attributes such as a caption, image source, and additional metadata. A specific photo would be stored in an instance of a model, and a model may also be reusable. Below, we can see an example of a very simplistic model implemented using Backbone.
var
Photo
=
Backbone
.
Model
.
extend
({
// Default attributes for the photo
defaults
:
{
src
:
"placeholder.jpg"
,
caption
:
"A default image"
,
viewed
:
false
},
// Ensure that each photo created has an `src`.
initialize
:
function
()
{
this
.
set
(
{
"src"
:
this
.
defaults
.
src
}
);
}
});
The built-in capabilities of models vary across frameworks,
however it is quite common for them to support validation of attributes,
where attributes represent the properties of the model, such as a model
identifier. When using models in real-world applications we generally
also desire model persistence. Persistence allows us to edit and update
models with the knowledge that its most recent state will be saved in
either memory, in a userâs localStorage
data store,
or synchronized with a database.
In addition, a model may have multiple views observing it. If, say, our photo model contained metadata, such as its location (longitude and latitude), friends who were present in the photo (a list of identifiers), and a list of tags, a developer may decide to provide a single view to display each of these three facets.
It is not uncommon for modern MVC/MV* frameworks to provide a means to group models together (e.g., in Backbone, these groups are referred to as âcollectionsâ). Managing models in groups allows us to write application logic based on notifications from the group should any model it contains be changed. This avoids the need to manually observe individual model instances.
A sample grouping of models into a simplified Backbone collection can be seen here.
var
PhotoGallery
=
Backbone
.
Collection
.
extend
({
// Reference to this collection's model.
model
:
Photo
,
// Filter down the list of all photos
// that have been viewed
viewed
:
function
()
{
return
this
.
filter
(
function
(
photo
){
return
photo
.
get
(
"viewed"
);
});
},
// Filter down the list to only photos that
// have not yet been viewed
unviewed
:
function
()
{
return
this
.
without
.
apply
(
this
,
this
.
viewed
()
);
}
});
Older texts on MVC may also refer to a notion of models managing application state. In JavaScript applications, state has a different connotation, typically referring to the current âstateââi.e., view or subview (with specific data) on a users screen at a fixed point. State is regularly discussed when looking at single-page applications, where the concept of state needs to be simulated.
So to summarize, models are primarily concerned with business data.
Views are a visual representation of models that present a filtered view of their current state. While Smalltalk views are about painting and maintaining a bitmap, JavaScript views are about building and maintaining a DOM element.
A view typically observes a model and is notified when the model changes, allowing the view to update itself accordingly. Design pattern literature commonly refers to views as âdumb,â given that their knowledge of models and controllers in an application is limited.
Users are able to interact with views, and this includes the ability to read and edit (i.e., get or set the attribute values in) models. As the view is the presentation layer, we generally present the ability to edit and update in a user-friendly fashion. For example, in the former photo gallery application we discussed earlier, model editing could be facilitated through an âeditâ view where a user who has selected a specific photo could edit its metadata.
The actual task of updating the model falls to controllers (which we will be covering shortly).
Letâs explore views a little further using a vanilla JavaScript sample implementation. Below we can see a function that creates a single photo view, consuming both a model instance and a controller instance.
We define a render()
utility
within our view, which is responsible for rendering the contents of the
photoModel
using a JavaScript
templating engine (Underscore templating) and updating the contents of
our view, referenced by photoEl
.
The photoModel
then adds our
render()
callback as one of its
subscribers so that through the Observer pattern we can trigger the view
to update when the model changes.
One may wonder where user interaction comes into play here. When
users click on any elements within the view, itâs not the viewâs
responsibility to know what to do next. It relies on a controller to
make this decision for it. In our sample implementation, this is
achieved by adding an event listener to photoEl
, which will delegate handling the
click behavior back to the controller, passing the model information
along with it in case itâs needed.
The benefit of this architecture is that each component plays its own separate role in making the application function as needed.
var
buildPhotoView
=
function
(
photoModel
,
photoController
)
{
var
base
=
document
.
createElement
(
"div"
),
photoEl
=
document
.
createElement
(
"div"
);
base
.
appendChild
(
photoEl
);
var
render
=
function
()
{
// We use a templating library such as Underscore
// templating which generates the HTML for our
// photo entry
photoEl
.
innerHTML
=
_
.
template
(
"#photoTemplate"
,
{
src
:
photoModel
.
getSrc
()
});
};
photoModel
.
addSubscriber
(
render
);
photoEl
.
addEventListener
(
"click"
,
function
()
{
photoController
.
handleEvent
(
"click"
,
photoModel
);
});
var
show
=
function
()
{
photoEl
.
style
.
display
=
""
;
};
var
hide
=
function
()
{
photoEl
.
style
.
display
=
"none"
;
};
return
{
showView
:
show
,
hideView
:
hide
};
};
In the context of JavaScript frameworks that support MVC/MV*, it is worth briefly discussing JavaScript templating and its relationship to views, as we briefly touched upon it in the last section.
It has long been considered (and proven) a performance bad
practice to manually create large blocks of HTML markup in memory
through string concatenation. Developers doing so have fallen prey to
unperformant iterating through their data, wrapping it in nested
divs
, and using outdated techniques such as
document.write
to inject the
âtemplateâ into the DOM. As this typically means keeping scripted
markup inline with our standard markup, it can quickly become both
difficult to read and, more importantly, maintain such disasters,
especially when building nontrivially sized applications.
JavaScript templating solutions (such as Handlebars.js and
Mustache) are often used to define templates for views as markup
(either stored externally or within script tags with a custom
typeâe.g., text/template
) containing template
variables. Variables may be delimited using a variable syntax (e.g.,
{{name}}
), and frameworks are typically smart
enough to accept data in a JSON form (which model instances can be
converted to), such that we only need be concerned with maintaining
clean models and clean templates. Most of the grunt work to do with
population is taken care of by the framework itself. This has a large
number of benefits, particularly when opting to store templates
externally, as this can give way to templates being dynamically loaded
on an as-needed basis when it comes to building larger
applications.
Here, we can see two examples of HTML templates (Examples 10-1 and 10-2). One implemented using the popular Handlebars.js framework and another using Underscoreâs templates.
Note that templates are not themselves views. Developers coming from a Struts Model 2 architecture may feel like a template is a view, but it isnât. A view is an object that observes a model and keeps the visual representation up to date. A template might be a declarative way to specify part or even all of a view object so that it can be generated from the template specification.
It is also worth noting that in classical web development, navigating between independent views required the use of a page refresh. In single-page JavaScript applications, however, once data is fetched from a server via Ajax, it can simply be dynamically rendered in a new view within the same page without any such refresh being necessary. The role of navigation thus falls to a router, which assists in managing application state (e.g., allowing users to bookmark a particular view they have navigated to). As routers are however neither a part of MVC nor present in every MVC-like framework, I will not be going into them in greater detail in this section.
To summarize, views are a visual representation of our application data.
Controllers are intermediaries between models and views, which are classically responsible for updating the model when the user manipulates the view.
In our photo gallery application, a controller would be responsible for handling changes the user made to the edit view for a particular photo, updating a specific photo model when a user has finished editing.
Remember that the controllers fulfill one role in MVC: the facilitation of the Strategy pattern for the view. In the Strategy pattern regard, the view delegates to the controller at the viewâs discretion; thatâs how the strategy pattern works. The view could delegate handling user events to the controller when the view sees fit. The view could delegate handling model change events to the controller if the view sees fit, but this is not the traditional role of the controller.
In terms of where most JavaScript MVC frameworks detract from what is conventionally considered âMVCâ however, it is with controllers. The reasons for this vary, but in my honest opinion, it is that framework authors initially look at the server-side interpretation of MVC, realize that it doesnât translate 1:1 on the client side, and reinterpret the C in MVC to mean something they feel makes more sense. The issue with this, however, is that it is subjective and increases the complexity in both understanding the classical MVC pattern and the role of controllers in modern frameworks.
As an example, letâs briefly review the architecture of the popular architectural framework Backbone.js. Backbone contains models and views (somewhat similar to what we reviewed earlier); however, it doesnât actually have true controllers. Its views and routers act a little similar to a controller, but neither are actually controllers on their own.
In this respect, contrary to what might be mentioned in the official documentation or in blog posts, Backbone is neither a truly MVC/MVP nor MVVM framework. Itâs in fact better to consider it a member of the MV* family that approaches architecture in its own way. There is of course nothing wrong with this, but it is important to distinguish between classical MVC and MV*, should we begin relying on advice from classical literature on the former to help with the latter.
We now know that controllers are traditionally responsible for updating the model when the user updates the view. Itâs interesting to note that the most popular JavaScript MVC/MV* framework at the time of writing (Backbone) does not have its own explicit concept of controllers.
It can thus be useful for us to review the controller from another MVC framework to appreciate the difference in implementations and further demonstrate how nontraditionally frameworks approach the role of the controller. For this, letâs take a look at a sample controller from Spine.js.
In this example, weâre going to have a controller called
PhotosController
, which will be in
charge of individual photos in the application. It will ensure that
when the view updates (e.g., a user edited the photo metadata), the
corresponding model does, too.
We wonât be delving heavily into Spine.js at all, but will just take a 10-foot view of what its controllers can do:
// Controllers in Spine are created by inheriting from Spine.Controller
var
PhotosController
=
Spine
.
Controller
.
sub
({
init
:
function
()
{
this
.
item
.
bind
(
"update"
,
this
.
proxy
(
this
.
render
));
this
.
item
.
bind
(
"destroy"
,
this
.
proxy
(
this
.
remove
));
},
render
:
function
()
{
// Handle templating
this
.
replace
(
$
(
"#photoTemplate"
).
tmpl
(
this
.
item
)
);
return
this
;
},
remove
:
function
()
{
this
.
el
.
remove
();
this
.
release
();
}
});
In Spine, controllers are considered the glue for an application, adding and responding to DOM events, rendering templates, and ensuring that views and models are kept in sync (which makes sense in the context of what we know to be a controller).
What weâre doing in the above example is setting up listeners in
the update
and destroy
events using render()
and remove()
. When a photo entry gets updated,
we re-render the view to reflect the changes to the metadata.
Similarly, if the photo gets deleted from the gallery, we remove it
from the view. In the render()
function, weâre using Underscore microtemplating (via _.template()
) to render a JavaScript
template with the ID #photoTemplate
. This simply
returns a compiled HTML string used to populate the contents of
photoEl
.
What this provides us with is a very lightweight, simple way to manage changes between the model and the view.
Later in this section, weâre going to revisit the differences between Backbone and traditional MVC, but for now, letâs focus on controllers.
In Backbone, one shares the responsibility of a controller with
both Backbone.View
and Backbone.Router
. Some time ago, Backbone did
come with its own Backbone.Controller
, but as the naming for
this component didnât make sense for the context in which it was being
used, it was later renamed to Router
.
Routers handle a little more of the controller responsibility,
as itâs possible to bind the events there for models and have our view
respond to DOM events and rendering. As Tim Branyen (another
Bocoup-based Backbone contributor) has also previously pointed out,
itâs possible to get away with not needing Backbone.Router
at all for this, so a way to
think about it using the Router paradigm
is
probably:
var
PhotoRouter
=
Backbone
.
Router
.
extend
({
routes
:
{
"photos/:id"
:
"route"
},
route
:
function
(
id
)
{
var
item
=
photoCollection
.
get
(
id
);
var
view
=
new
PhotoView
(
{
model
:
item
}
);
$
(
'.content'
).
html
(
view
.
render
().
el
);
}
});
To summarize, the takeaway from this section is that controllers manage the logic and coordination between models and views in an application.
Get Learning JavaScript Design Patterns now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.