Create a gist now

Instantly share code, notes, and snippets.

@jdudek /app.coffee Secret
Created Jan 28, 2012

class Application
constructor: (root, facebook) ->
popupQueue = new PopupQueue(new PopupQueueView)
popupManager = new PopupManager(popupQueue, {
about: -> new AboutPopup(new AboutPopupView)
wallPost: -> new WallPostPopup(facebook)
})
mainController = new MainController(new MainView(root), popupManager, facebook)
mainController.append(popupQueue)
@run = -> mainController.display()
class MainController
constructor: (@view, @popupManager, @facebook) ->
@view.bind "about:clicked", => @popupManager.display("about")
@view.bind "wallPost:clicked", => @popupManager.display("wallPost")
@view.bind "both:clicked", =>
@popupManager.display("wallPost")
@popupManager.display("about")
@view.bind "login:clicked", =>
@facebook.login().done =>
@view.showLinksForLoggedIn()
@view.hideLogin()
display: =>
@view.display()
@facebook.initialize().done =>
@view.showLogin()
append: (controller) =>
@view.append(controller.view)
class MainView
constructor: (@root) ->
$.extend(@, new Observable)
@elem = $(this.getHtml())
@elem.find("a.login, .whenLoggedIn").hide()
@elem.on "click", "div.menu a", (e) => e.preventDefault()
@elem.on "click", "a.about", (e) => @trigger("about:clicked")
@elem.on "click", "a.wallPost", (e) => @trigger("wallPost:clicked")
@elem.on "click", "a.both", (e) => @trigger("both:clicked")
@elem.on "click", "a.login", (e) => @trigger("login:clicked")
getHtml: =>
"""
<div class="app">
<h1>Popups in MVC - example</h1>
<div class="menu">
<a href="#" class="about">About...</a>
<span class="whenLoggedIn">
<a href="#" class="wallPost">Post to wall popup</a>
<a href="#" class="both">Both popups</a>
</span>
<a href="#" class="login">Login via Facebook</a>
</div>
<ul>
<li><a href="#">read source</a></li>
<li><a href="./tests.html">run tests</a></li>
</ul>
</div>
"""
display: =>
@elem.appendTo(@root)
append: (subview) =>
@elem.append(subview.elem)
showLinksForLoggedIn: =>
@elem.find(".whenLoggedIn").show()
showLogin: =>
@elem.find("a.login").show()
hideLogin: =>
@elem.find("a.login").hide()
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...)
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) =>
if popup.display?
popup.display()
else
@view.append(popup.view)
hidePopup: (popup) =>
if popup.hide?
popup.hide()
else
@view.remove(popup.view)
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()
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", (e) => e.preventDefault()
@elem.on "click", "a.close", (e) => @trigger("close:clicked")
getHtml: =>
"""
<div class="popup about">
<p>This is about popup</p>
<a href="#" class="close">Close</a>
</div>
"""
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: =>
class FacebookAdapter
constructor: (@appId) ->
initialize: =>
$.Deferred (dfr) =>
window.fbAsyncInit = =>
FB.init({ appId: @appId, status: true, cookie: true, xfbml: true })
dfr.resolve()
$('<div id="fb-root"></div>').appendTo("body")
$('<script src="//connect.facebook.net/en_US/all.js" async></script>').appendTo("body")
.promise()
login: =>
$.Deferred (dfr) =>
if FB.getAuthResponse()
dfr.resolve()
else
FB.login (response) ->
if response.authResponse
dfr.resolve()
else
dfr.reject()
.promise()
showWallPostDialog: (options, callback) =>
defaults =
method: 'feed'
display: 'dialog'
options = $.extend(defaults, options)
FB.ui(options, callback)
# Export classes outside this file:
# - Application & FacebookAdapter are needed to run app.
# - PopupQueue & PopupManager will be unit-tested.
window.Application = Application
window.FacebookAdapter = FacebookAdapter
window.PopupQueue = PopupQueue
window.PopupManager = PopupManager
FB_APP_ID = "321934104511312"
facebook = new FacebookAdapter(FB_APP_ID)
root = $("body")
app = new Application(root, facebook)
app.run()
describe "acceptance", ->
scenario = it
beforeEach ->
@root = $("<div></div>").appendTo("body")
@facebook = mockFacebookAdapter()
@app = new Application(@root, @facebook)
@app.run()
afterEach ->
@root.remove()
scenario "user enters the app and opens about popup", ->
expect(@root.find("h1")).toBeVisible()
expect(@root.find("h1")).toHaveText("Popups in MVC - example")
@root.find("a.about").click()
expect(@root.find(".popup.about")).toExist()
expect(@root.find(".popup.about")).toHaveText(/This is about popup/)
@root.find(".popup.about a.close").click()
expect(@root.find(".popup.about")).not.toExist()
scenario "user logs in via Facebook and opens wall post popup", ->
@facebook.initialize.resolve()
expect(@root.find("a.login")).toBeVisible()
expect(@root.find("a.wallPost")).not.toBeVisible()
@root.find("a.login").click()
expect(@facebook.login).toHaveBeenCalled()
@facebook.login.resolve()
expect(@root.find("a.wallPost")).toBeVisible()
@root.find("a.wallPost").click()
expect(@facebook.showWallPostDialog).toHaveBeenCalled()
scenario "user logs in via Facebook and opens both popups", ->
@facebook.initialize.resolve()
@root.find("a.login").click()
@facebook.login.resolve()
expect(@root.find("a.both")).toBeVisible()
@root.find("a.both").click()
expect(@facebook.showWallPostDialog).toHaveBeenCalled()
expect(@root.find(".popup.about")).not.toExist()
@facebook.closeWallPostDialog()
expect(@root.find(".popup.about")).toExist()
@root.find(".popup.about a.close").click()
expect(@root.find(".popup.about")).not.toExist()
scenario "user clicks login but rejects authorization", ->
@facebook.initialize.resolve()
expect(@root.find("a.wallPost")).not.toBeVisible()
@root.find("a.login").click()
@facebook.login.reject()
expect(@root.find("a.wallPost")).not.toBeVisible()
scenario "Facebook initialization fails", ->
@facebook.initialize.reject()
expect(@root.find("a.login")).not.toBeVisible()
expect(@root.find("a.wallPost")).not.toBeVisible()
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()
describe "when popup manages display/hide itself", ->
beforeEach ->
@popup = $.extend({ view: {}, display: (->), hide: (->) }, new Observable)
spyOn(@popup, "display")
spyOn(@popup, "hide")
@popupQueue.register(@popup)
it "should not append the view", ->
expect(@popupQueueView.append).not.toHaveBeenCalled()
it "should call display on popup", ->
expect(@popup.display).toHaveBeenCalled()
describe "when popup is closed", ->
beforeEach ->
@popup.trigger("closed")
it "should not remove the view", ->
expect(@popupQueueView.remove).not.toHaveBeenCalled()
it "should call hide on popup", ->
expect(@popup.hide).toHaveBeenCalled()
describe "PopupManager", ->
beforeEach ->
@aboutPopup = {}
@aboutPopupFactory = jasmine.createSpy().andCallFake(=> @aboutPopup)
@popupQueue = jasmine.createSpyObj("popupQueue", ["register"])
@popupManager = new PopupManager(@popupQueue, { "about": @aboutPopupFactory })
describe "create", ->
it "should pass arguments to the factory", ->
@popupManager.create("about", 1, 2, 3)
expect(@aboutPopupFactory).toHaveBeenCalledWith(1, 2, 3)
describe "display", ->
it "should register popups in queue", ->
@popupManager.display("about")
expect(@popupQueue.register).toHaveBeenCalledWith(@aboutPopup)
# This may be not the simplest way to mock FacebookAdapter class, but I find
# the resulting test code to be quite elegant
mockFacebookAdapter = ->
obj = jasmine.createSpyObj("facebook", ["initialize", "login", "showWallPostDialog"])
for m in ["initialize", "login"]
do (m) ->
obj[m].andCallFake ->
dfr = new $.Deferred()
unset = ->
delete obj[m].resolve
delete obj[m].reject
obj[m].resolve = -> unset() ; dfr.resolve()
obj[m].reject = -> unset() ; dfr.reject()
return dfr
obj.closeWallPostDialog = ->
onClose = obj.showWallPostDialog.mostRecentCall.args[1]
onClose()
return obj
@jdudek
Owner
jdudek commented Jan 29, 2012

This is sample code for a blog post: http://jandudek.com/2012/01/29/implementing-popups-in-diy-mvc.html (please forgive this circular reference)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment