Implementing popups in do-it-yourself MVC
The goal of this post is to describe how I've implemented a (not very complex) feature in one of my projects. I hope it will give you a peek into my architecture choices and show how to build a JavaScript MVC app without MVC framework.
Some ideas presented here were developed in our team while working on Gameboxed, but this post does not necessarily reflect opinions of my teammates :)
We'll use jQuery - but only in the view layer, except utilities like $.extend. We also need implementation of observer pattern (sometimes called EventEmitter) - the examples use my own, tiny Observable library, but you could just as well borrow Backbone.Events.
At the end of the post you'll find links to working example and source code.
Ok, let's begin.
What is a popup in terms of MVC? Popup is a kind of widget. Widget consists of a view and a controller. Each time we want to display a popup, we'll have a controller object of class with name ending *Popup and another one - view - with name like *PopupView. The controller can talk to the model and receive events from the view. The view should be dumb.
Who will start displaying a popup? In my understanding of MVC, this is role of the controller, often in response to an event from the view.
For example:
class MainController
constructor: (@view) ->
@view.bind "about:clicked", =>
popup = new AboutPopup(new AboutPopupView)
@view.append(popup.view)
This example looks a bit too verbose and keeps a hardcoded dependency on AboutPopup and AboutPopupView classes. If you've read my last post about factories, you might already know how I'd solve it:
class MainController
constructor: (@view, createAboutPopup) ->
@view.bind "about:clicked", =>
popup = createAboutPopup()
@view.append(popup.view)
# in application bootstrap:
createAboutPopup = -> new AboutPopup(new AboutPopupView)
mainController = new MainController(new MainView, createAboutPopup)
We want to be able to display many popups, but not simultanously, rather one after the other. They cannot overlap, becuase this is ugly and confusing. That's why we'll have a queue object, responsible for showing next popup after one is closed (which they'll indicate by triggering closed event).
class MainController
constructor: (@view, @popupQueue, createAboutPopup, createAnotherPopup) ->
@view.bind "about:clicked", =>
@popupQueue.register(createAboutPopup())
@popupQueue.register(createAnotherPopup())
Popups should be modal, which means other parts of interface have to be blocked while popup is visible. We'll achieve this using an "overlay" - semi-transparent div laid over page content. We want to have only one overlay, so its appearance will also be managed by queue. This leads us to the conclusion, that popup queue is also a kind of widget - it consists of a controller (PopupQueue) and a view (PopupQueueView).
Ok, enough philosophy, back to reality. Here's Jasmine spec for our queue:
describe "PopupQueue", ->
beforeEach ->
@popupQueueView = jasmine.createSpyObj("popupQueueView",
["showOverlay", "hideOverlay", "append", "remove"])
@popupQueue = new PopupQueue(@popupQueueView)
describe "when registered two popups", ->
beforeEach ->
mockPopupView = -> new Observable
mockPopup = (view) -> $.extend({ view: view }, new Observable)
@popup1View = mockPopupView()
@popup1 = mockPopup(@popup1View)
@popup2View = mockPopupView()
@popup2 = mockPopup(@popup2View)
@popupQueue.register(@popup1)
@popupQueue.register(@popup2)
it "should display overlay", ->
expect(@popupQueueView.showOverlay).toHaveBeenCalled()
it "should display only first popup view", ->
expect(@popupQueueView.append.callCount).toEqual(1)
expect(@popupQueueView.append.mostRecentCall.args[0]).toBe(@popup1View)
describe "when first popup is closed", ->
beforeEach ->
@popup1.trigger("closed")
it "should remove first popup view", ->
expect(@popupQueueView.remove.callCount).toEqual(1)
expect(@popupQueueView.remove.mostRecentCall.args[0]).toBe(@popup1View)
it "should display second popup view", ->
expect(@popupQueueView.append.callCount).toEqual(2)
expect(@popupQueueView.append.mostRecentCall.args[0]).toBe(@popup2View)
it "should not hide overlay", ->
expect(@popupQueueView.hideOverlay).not.toHaveBeenCalled()
describe "when second popup is closed", ->
beforeEach ->
@popup2.trigger("closed")
it "should remove second popup view", ->
expect(@popupQueueView.remove.callCount).toEqual(2)
expect(@popupQueueView.remove.mostRecentCall.args[0]).toBe(@popup2View)
it "should hide overlay", ->
expect(@popupQueueView.hideOverlay).toHaveBeenCalled()
The implementation should be left as an exercise for the reader, but I'll need it later in this post, so here's the code:
class PopupQueue
constructor: (@view) ->
@queue = []
register: (popup) =>
@queue.push(popup)
if @queue.length == 1
@view.showOverlay()
@processNext()
processNext: =>
if @queue.length > 0
popup = @queue[0]
popup.bind "closed", =>
@hidePopup(popup)
@queue.shift()
@processNext()
@displayPopup(popup)
else
@view.hideOverlay()
displayPopup: (popup) =>
@view.append(popup.view)
hidePopup: (popup) =>
@view.remove(popup.view)
And here's PopupQueueView:
class PopupQueueView
constructor: ->
@elem = $('<div class="popups"></div>')
showOverlay: =>
$('<div class="overlay"></div>').hide().appendTo(@elem).fadeIn()
hideOverlay: =>
@elem.find(".overlay").fadeOut(=> @elem.find(".overlay").remove())
append: (subview) =>
@elem.append(subview.elem)
remove: (subview) =>
@elem.find(subview.elem).remove()
Is it dumb? I think it is. I don't feel the need to unit-test such views. I'll click through the application anyway to see if animation looks good and CSS is OK.
Finally, we'll need the Popup and PopupView classes. They're fairly straithforward:
class AboutPopup
constructor: (@view) ->
$.extend(@, new Observable)
@view.bind "close:clicked", => @trigger("closed")
class AboutPopupView
constructor: ->
$.extend(@, new Observable)
@elem = $(this.getHtml())
@elem.on "click", "a.close", (e) =>
e.preventDefault()
@trigger("close:clicked")
getHtml: =>
"""
<div class="popup about">
<a href="#" class="close">Close</a>
</div>
"""
The controller (AboutPopup) forwards event triggered when user clicks "Close", the queue will catch it and remove the view.
So, to sum up:
- we divide popups to controllers and views
- we use a queue that orchestrates their visibility
The benefits of this approach:
- Popup as a controller may contain any complex logic, depend on many models etc. - anything you need. The caller doesn't know anything about it. The view may be just a message, may contain complex HTML, use localization, and so on.
- It's easy to reuse popup classes with different views, e.g. one controller for class with just "close" button, but different views for different content. Popups are objects, so we can use any OO techniques to make them DRY and still maintainable.
Basically, every time we need some custom behaviour, we have a place to put it in. How often do we need it? In my last application, from which these examples come from, 7 of 10 popups were "custom" and needed specialized classes.
There's more...
We'll squeeze even more from this code. Suppose your app uses Facebook JavaScript SDK and you want to display wall post dialog - something like this:
Seems like another kind of popup, don't you think? It would be great to put them in our queue, so they're modal (just like any other popup, with overlay blocking rest of the interface) and so we can easily display them between other popups. In my opinion this results in much better user experience. We'll also have just one API to display popups, with no exception for those Facebook ones.
The (small) problem is that Facebook SDK does not follow MVC pattern, and even if it did, it would not follow our conventions with appending subviews. So one of our popups won't be really MVC, but it doesn't bother us, because no-one will know (maybe except PopupQueue). This is the kind of "code smells" that I don't care about - when our dirty implementation does not leak anywhere else, because the interface is clean.
The only changes are that displayPopup/hidePopup in PopupQueue have to be altered to support popups that will display/hide themselves, just like our Facebook ones. We discover whether a popup manages itself by looking if it supports display/hide methods.
displayPopup: (popup) =>
if popup.display?
popup.display()
else
@view.append(popup.view)
hidePopup: (popup) =>
if popup.hide?
popup.hide()
else
@view.remove(popup.view)
And here's our example wall-post-popup:
class WallPostPopup
constructor: (@facebook) ->
$.extend(@, new Observable)
display: =>
message = "I'm reading about JavaScript MVC on JanDudek.com"
description = "Read more about JavaScript, Ruby and web development"
options =
link: "http://jandudek.com"
name: message
description: description
callback = => @trigger("closed")
@facebook.showWallPostDialog(options, callback)
hide: =>
# hide method is just to indicate that this popup will manage hiding by itself
Note that we don't call FB.ui directly, but use an adapter object. This approach simplifies testing and makes much clearer what are dependencies of our class (everything listed in constructor arguments). Below is the relevant snippet of our adapter:
class FacebookAdapter
# ...
showWallPostDialog: (options, callback) =>
defaults =
method: 'feed'
display: 'dialog'
options = $.extend(defaults, options)
FB.ui(options, callback)
Still reading?
Ok, here's another issue. Some objects will need to show many popups, 3 or 4. So they'll receive 4 factories in constructor + popupQueue object. Add a few other dependencies and suddenly arguments list of your class' constructor spans three lines of code.
Also, maybe knowing that there's a queue and popup objects is just too much knowledge for others? Basically you just want to fire a popup and don't care what will happen.
That's why I decided to introduce another class: a PopupManager (yeah, great name...). It knows about any kind of popup and you just call one method to display what you need.
popupQueue = new PopupQueue(new PopupQueueView)
popupManager = new PopupManager(popupQueue, {
about: -> new AboutPopup(new AboutPopupView)
wallPost: -> new WallPostPopup
})
class MainController
constructor: (@view, @popupManager) ->
@view.bind "about:clicked", =>
@popupManager.display("about")
@popupManager.display("wallPost")
Here's the implementation:
class PopupManager
constructor: (@queue, @factories) ->
display: (args...) =>
popup = @create(args...)
@register(popup)
register: (popup) =>
@queue.register(popup)
create: (args...) =>
name = args.shift()
factory = @factories[name]
factory(args...)
If the list of factories passed to manager grows too large, or the manager exposes too many kinds of popups for other objects, you can split factories list and have many managers sharing one queue.
That's all, folks!
I hope that you've found something useful in this long blog post, even if you're experienced JavaScript developer. Feel free to leave comments if you have any questions. Or if you think it was total bullshit :-)
I've also prepared a a working example (with tests). Here's its source. In the example code there are many details and techniques that are beyond scope of this blog post. Again, feel free to ask in comments if you have any questions.