Skip to content

Instantly share code, notes, and snippets.

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

Embed
What would you like to do?
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

This comment has been minimized.

Copy link
Owner Author

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
You can’t perform that action at this time.