-
-
Save jdudek/a2144a9174db65049e1d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
FB_APP_ID = "321934104511312" | |
facebook = new FacebookAdapter(FB_APP_ID) | |
root = $("body") | |
app = new Application(root, facebook) | |
app.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
jasmine.Spy.prototype.andReturnDeferred = -> | |
@deferreds = [] | |
@resolve = => | |
unless @deferreds.length > 0 | |
throw "Cannot resolve, #{@identity} was not called yet" | |
dfr = @deferreds.shift() | |
dfr.resolve() | |
@reject = => | |
unless @deferreds.length > 0 | |
throw "Cannot reject, #{@identity} was not called yet" | |
dfr = @deferreds.shift() | |
dfr.reject() | |
@andCallFake => | |
dfr = new $.Deferred | |
@deferreds.push(dfr) | |
dfr | |
mockFacebookAdapter = -> | |
obj = jasmine.createSpyObj("facebook", ["initialize", "login", "showWallPostDialog"]) | |
obj.initialize.andReturnDeferred() | |
obj.login.andReturnDeferred() | |
obj.closeWallPostDialog = -> | |
onClose = obj.showWallPostDialog.mostRecentCall.args[1] | |
onClose() | |
return obj |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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)