├── .gitignore ├── images ├── ajax.gif ├── bacon.png └── registration-form-bacon.png ├── mocks.js ├── lib ├── Bacon.UI.js ├── jquery.mockjax.js └── Bacon.js ├── index.html ├── README.md └── css └── register.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /images/ajax.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/bacon-devday-code/HEAD/images/ajax.gif -------------------------------------------------------------------------------- /images/bacon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/bacon-devday-code/HEAD/images/bacon.png -------------------------------------------------------------------------------- /images/registration-form-bacon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raimohanska/bacon-devday-code/HEAD/images/registration-form-bacon.png -------------------------------------------------------------------------------- /mocks.js: -------------------------------------------------------------------------------- 1 | $.mockjax({ 2 | url: '/usernameavailable/*', 3 | dataType: 'json', 4 | response: function(settings) { 5 | var username = settings.url.match(/.+\/(\w+$)/)[1] 6 | var available = username.length % 2 == 0 7 | this.responseText = available; 8 | } 9 | }); 10 | 11 | $.mockjax({ 12 | url: "/register", 13 | dataType: "json", 14 | type: "post", 15 | response: function(settings) { 16 | this.responseText = "true" 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /lib/Bacon.UI.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var isChrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1; 3 | function nonEmpty(x) { return x && x.length > 0 } 4 | 5 | Bacon.UI = {} 6 | Bacon.UI.textFieldValue = function(textfield, initValue) { 7 | function getValue() { return textfield.val() } 8 | function autofillPoller() { 9 | if (textfield.attr("type") == "password") 10 | return Bacon.interval(100) 11 | else if (isChrome) 12 | return Bacon.interval(100).take(20).map(getValue).filter(nonEmpty).take(1) 13 | else 14 | return Bacon.never() 15 | } 16 | if (initValue !== null) { 17 | textfield.val(initValue) 18 | } 19 | return textfield.asEventStream("keyup input"). 20 | merge(textfield.asEventStream("cut paste").delay(1)). 21 | merge(autofillPoller()). 22 | map(getValue).toProperty(getValue()).skipDuplicates() 23 | } 24 | Bacon.UI.optionValue = function(option) { 25 | function getValue() { return option.val() } 26 | return option.asEventStream("change").map(getValue).toProperty(getValue()) 27 | } 28 | Bacon.UI.checkBoxGroupValue = function(checkboxes, initValue) { 29 | function selectedValues() { 30 | return checkboxes.filter(":checked").map(function(i, elem) { return $(elem).val()}).toArray() 31 | } 32 | if (initValue) { 33 | checkboxes.each(function(i, elem) { 34 | $(elem).attr("checked", initValue.indexOf($(elem).val()) >= 0) 35 | }) 36 | } 37 | return checkboxes.asEventStream("click").map(selectedValues).toProperty(selectedValues()) 38 | } 39 | Bacon.Observable.prototype.pending = function(src) { 40 | return src.map(true).merge(this.map(false)).toProperty(false) 41 | } 42 | Bacon.EventStream.prototype.ajax = function() { 43 | return this["switch"](function(params) { return Bacon.fromPromise($.ajax(params)) }) 44 | } 45 | Bacon.UI.radioGroupValue = function(options) { 46 | var initialValue = options.filter(':checked').val() 47 | return options.asEventStream("change").map('.target.value').toProperty(initialValue) 48 | } 49 | })(); 50 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 39 | 40 | 41 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is the Bacon.JS coding excercise we did at ReaktorDevDay 2012. 2 | Below are the instructions in case you wanna try it yourself. You may also 3 | have a look at the [Full Solution](https://github.com/raimohanska/bacon-devday-code/tree/full-solution) which 4 | is in another branch. 5 | 6 | ## Preparations 7 | 8 | 1. Clone this repo 9 | 10 | ~~~ 11 | git clone https://github.com/raimohanska/bacon-devday-code.git 12 | cd bacon-devday-code 13 | ~~~ 14 | 15 | 2. Open the index.html file in your browser 16 | 17 | ~~~ 18 | open index.html 19 | ~~~ 20 | 21 | 3. Make sure you have developer tools in your browser and that you can 22 | use them. Google Chrome will do. In Chrome (Mac OSX), Go to View -> 23 | Developer -> Developer Tools. You should be able to run Javascript 24 | expressions on the Console tab. 25 | 26 | 4. Try some expression in the Developer Console, like 27 | 28 | ~~~ 29 | $("#username input").asEventStream("keyup") 30 | ~~~ 31 | 32 | 5. Have a look at Bacon.js 33 | [readme](https://github.com/raimohanska/bacon.js/blob/master/README.md) 34 | 35 | ## Map 36 | 37 | Here's how I modeled the problem for Bacon.js reactive code. 38 | 39 |  40 | 41 | Side-effects are not depicted. 42 | 43 | ## Steps to success 44 | 45 | 1. Disable button if username is missing 46 | * define usernameEntered property 47 | * assign side-effect: setDisabled for registerButton 48 | 49 | 2. Disable also if full name is missing 50 | * define fullname and fullnameEntered properties 51 | * use .and() to change the condition for enabling the button 52 | 53 | 3. Disable also if username unavailable 54 | * include usernameAvailable to the condition for enabling the button 55 | 56 | 4. Show AJAX indicator when AJAX pending 57 | * define usernameRequestPending property as usernameResponse.pending(usernameRequest) 58 | * assign side effect to show usernameAjaxIndicator 59 | 60 | 5. Disable button when AJAX is pending 61 | 62 | 6. Implement registerClick stream 63 | * tip: do(".preventDefault") 64 | 65 | 7. Implement registrationRequest 66 | * combine username and password into a new property that would be the data given to JQuery.ajax 67 | * can use username.combine(..) or Bacon.combineTemplate 68 | * type: "POST" 69 | 70 | 8. Make this a stream of registration requests, send when the button is clicked 71 | * .sampledBy(registerClick) 72 | 73 | 9. Create registrationResponse stream 74 | * as in usernameResponse stream 75 | 76 | 10. Show feedback 77 | 78 | 11. Disable button after registration sent 79 | 80 | 12. Show ajax indicator for registration POST 81 | -------------------------------------------------------------------------------- /css/register.css: -------------------------------------------------------------------------------- 1 | body, input, button, h1 { 2 | font-family: 'Comic Sans MS', sans-serif; 3 | } 4 | 5 | .ajax { 6 | background: url('../images/ajax.gif') no-repeat; 7 | display: inline-block; 8 | width: 54; 9 | height: 54; 10 | margin-top: -2px; 11 | } 12 | 13 | #username-unavailable { 14 | display: none; 15 | } 16 | 17 | #username { 18 | position: relative; 19 | } 20 | 21 | #username .ajax { 22 | position: absolute; 23 | top: 10px; 24 | right: 0px; 25 | } 26 | 27 | .tooltip { 28 | font-size:1.7em; 29 | color: #fff; 30 | display:inline; 31 | padding:15px; 32 | position: absolute; 33 | left: 100%; 34 | top: 0%; 35 | white-space:nowrap; 36 | background-color: #FF0000; 37 | -webkit-border-radius: 10px 10px 10px 10px; 38 | -moz-border-radius: 10px 10px 10px 10px; 39 | border-radius: 10px 10px 10px 10px; 40 | border:2px solid #FF0000; 41 | -webkit-box-shadow: #B3B3B3 4px 4px 4px; 42 | -moz-box-shadow: #B3B3B3 4px 4px 4px; 43 | box-shadow: #B3B3B3 4px 4px 4px; 44 | z-index:97 45 | } 46 | 47 | .tooltip:before { 48 | border:solid; 49 | border-color:#FF0000 transparent; 50 | border-width:20px 20px 0 20px; 51 | top: 15px; 52 | left: -20px; 53 | content:""; 54 | display:block; 55 | position:absolute; 56 | z-index:98 57 | } 58 | 59 | #login-container { 60 | position: relative; 61 | width:400px; 62 | height:300px; 63 | margin:50px; 64 | padding: 0px 120px 40px 40px; 65 | background-color: #323B55; 66 | background-image: -webkit-linear-gradient(bottom, #323B55 0%, #424F71 100%); 67 | background-image: -moz-linear-gradient(bottom, #323B55 0%, #424F71 100%); 68 | background-image: -ms-linear-gradient(bottom, #323B55 0%, #424F71 100%); 69 | background-image: linear-gradient(bottom, #323B55 0%, #424F71 100%); 70 | -webkit-border-radius: 30px 40px 50px 90px; 71 | -moz-border-radius: 30px 40px 50px 90px; 72 | border-radius: 30px 40px 50px 90px; 73 | border:10px solid #F2F2F2; 74 | } 75 | 76 | #login-container:after { 77 | position:absolute; 78 | top:-50px; 79 | right:-100px; 80 | content: url('../images/bacon.png'); 81 | width: 200px; 82 | height: 176px; 83 | display: block; 84 | } 85 | 86 | #username, #fullname { 87 | position: relative; 88 | } 89 | 90 | #login-container h1 { 91 | color: #fff; 92 | } 93 | 94 | #login-container button { 95 | background-color: #FFFF00; 96 | border-color: #dddd00; 97 | color: #c00; 98 | } 99 | 100 | #login-container button:disabled { 101 | background-color: #888; 102 | color: #444; 103 | border-color: #666; 104 | } 105 | 106 | #login-container input { 107 | color: #800; 108 | } 109 | 110 | #login-container input::-webkit-input-placeholder { color: LightGray; } 111 | #login-container input:-moz-placeholder { color: LightGray; } 112 | #login-container input:-ms-input-placeholder { color: LightGray; } 113 | 114 | 115 | #login-container input, 116 | #login-container button { 117 | padding: 5px; 118 | font-size: 2.0em; 119 | margin-top: 10px; 120 | -webkit-border-radius: 15px 15px 15px 15px; 121 | -moz-border-radius: 15px 15px 15px 15px; 122 | border-radius: 15px 15px 15px 15px; 123 | border-width:3px; 124 | } 125 | #login-container input { 126 | width: 85%; 127 | } 128 | #login-container button { 129 | position: absolute; 130 | left: auto; 131 | right: -95px; 132 | } 133 | 134 | #register { 135 | position: relative; 136 | } 137 | 138 | #register .ajax { 139 | position: absolute; 140 | right: 81px; 141 | top: 12px; 142 | } 143 | 144 | #register #result { 145 | display: inline-block; 146 | margin-top: 10px; 147 | font-size: 2em; 148 | color: #ff0; 149 | text-shadow: black 4px 4px 4px; 150 | } 151 | -------------------------------------------------------------------------------- /lib/jquery.mockjax.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * MockJax - jQuery Plugin to Mock Ajax requests 3 | * 4 | * Version: 1.5.1 5 | * Released: 6 | * Home: http://github.com/appendto/jquery-mockjax 7 | * Author: Jonathan Sharp (http://jdsharp.com) 8 | * License: MIT,GPL 9 | * 10 | * Copyright (c) 2011 appendTo LLC. 11 | * Dual licensed under the MIT or GPL licenses. 12 | * http://appendto.com/open-source-licenses 13 | */ 14 | (function($) { 15 | var _ajax = $.ajax, 16 | mockHandlers = [], 17 | CALLBACK_REGEX = /=\?(&|$)/, 18 | jsc = (new Date()).getTime(); 19 | 20 | 21 | // Parse the given XML string. 22 | function parseXML(xml) { 23 | if ( window['DOMParser'] == undefined && window.ActiveXObject ) { 24 | DOMParser = function() { }; 25 | DOMParser.prototype.parseFromString = function( xmlString ) { 26 | var doc = new ActiveXObject('Microsoft.XMLDOM'); 27 | doc.async = 'false'; 28 | doc.loadXML( xmlString ); 29 | return doc; 30 | }; 31 | } 32 | 33 | try { 34 | var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' ); 35 | if ( $.isXMLDoc( xmlDoc ) ) { 36 | var err = $('parsererror', xmlDoc); 37 | if ( err.length == 1 ) { 38 | throw('Error: ' + $(xmlDoc).text() ); 39 | } 40 | } else { 41 | throw('Unable to parse XML'); 42 | } 43 | } catch( e ) { 44 | var msg = ( e.name == undefined ? e : e.name + ': ' + e.message ); 45 | $(document).trigger('xmlParseError', [ msg ]); 46 | return undefined; 47 | } 48 | return xmlDoc; 49 | } 50 | 51 | // Trigger a jQuery event 52 | function trigger(s, type, args) { 53 | (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args); 54 | } 55 | 56 | // Check if the data field on the mock handler and the request match. This 57 | // can be used to restrict a mock handler to being used only when a certain 58 | // set of data is passed to it. 59 | function isMockDataEqual( mock, live ) { 60 | var identical = false; 61 | // Test for situations where the data is a querystring (not an object) 62 | if (typeof live === 'string') { 63 | // Querystring may be a regex 64 | return $.isFunction( mock.test ) ? mock.test(live) : mock == live; 65 | } 66 | $.each(mock, function(k, v) { 67 | if ( live[k] === undefined ) { 68 | identical = false; 69 | return identical; 70 | } else { 71 | identical = true; 72 | if ( typeof live[k] == 'object' ) { 73 | return isMockDataEqual(mock[k], live[k]); 74 | } else { 75 | if ( $.isFunction( mock[k].test ) ) { 76 | identical = mock[k].test(live[k]); 77 | } else { 78 | identical = ( mock[k] == live[k] ); 79 | } 80 | return identical; 81 | } 82 | } 83 | }); 84 | 85 | return identical; 86 | } 87 | 88 | // Check the given handler should mock the given request 89 | function getMockForRequest( handler, requestSettings ) { 90 | // If the mock was registered with a function, let the function decide if we 91 | // want to mock this request 92 | if ( $.isFunction(handler) ) { 93 | return handler( requestSettings ); 94 | } 95 | 96 | // Inspect the URL of the request and check if the mock handler's url 97 | // matches the url for this ajax request 98 | if ( $.isFunction(handler.url.test) ) { 99 | // The user provided a regex for the url, test it 100 | if ( !handler.url.test( requestSettings.url ) ) { 101 | return null; 102 | } 103 | } else { 104 | // Look for a simple wildcard '*' or a direct URL match 105 | var star = handler.url.indexOf('*'); 106 | if (handler.url !== requestSettings.url && star === -1 || 107 | !new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace('*', '.+')).test(requestSettings.url)) { 108 | return null; 109 | } 110 | } 111 | 112 | // Inspect the data submitted in the request (either POST body or GET query string) 113 | if ( handler.data && requestSettings.data ) { 114 | if ( !isMockDataEqual(handler.data, requestSettings.data) ) { 115 | // They're not identical, do not mock this request 116 | return null; 117 | } 118 | } 119 | // Inspect the request type 120 | if ( handler && handler.type && 121 | handler.type.toLowerCase() != requestSettings.type.toLowerCase() ) { 122 | // The request type doesn't match (GET vs. POST) 123 | return null; 124 | } 125 | 126 | return handler; 127 | } 128 | 129 | // If logging is enabled, log the mock to the console 130 | function logMock( mockHandler, requestSettings ) { 131 | var c = $.extend({}, $.mockjaxSettings, mockHandler); 132 | if ( c.log && $.isFunction(c.log) ) { 133 | c.log('MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url, $.extend({}, requestSettings)); 134 | } 135 | } 136 | 137 | // Process the xhr objects send operation 138 | function _xhrSend(mockHandler, requestSettings, origSettings) { 139 | 140 | // This is a substitute for < 1.4 which lacks $.proxy 141 | var process = (function(that) { 142 | return function() { 143 | return (function() { 144 | // The request has returned 145 | this.status = mockHandler.status; 146 | this.statusText = mockHandler.statusText; 147 | this.readyState = 4; 148 | 149 | // We have an executable function, call it to give 150 | // the mock handler a chance to update it's data 151 | if ( $.isFunction(mockHandler.response) ) { 152 | mockHandler.response(origSettings); 153 | } 154 | // Copy over our mock to our xhr object before passing control back to 155 | // jQuery's onreadystatechange callback 156 | if ( requestSettings.dataType == 'json' && ( typeof mockHandler.responseText == 'object' ) ) { 157 | this.responseText = JSON.stringify(mockHandler.responseText); 158 | } else if ( requestSettings.dataType == 'xml' ) { 159 | if ( typeof mockHandler.responseXML == 'string' ) { 160 | this.responseXML = parseXML(mockHandler.responseXML); 161 | } else { 162 | this.responseXML = mockHandler.responseXML; 163 | } 164 | } else { 165 | this.responseText = mockHandler.responseText; 166 | } 167 | if( typeof mockHandler.status == 'number' || typeof mockHandler.status == 'string' ) { 168 | this.status = mockHandler.status; 169 | } 170 | if( typeof mockHandler.statusText === "string") { 171 | this.statusText = mockHandler.statusText; 172 | } 173 | // jQuery < 1.4 doesn't have onreadystate change for xhr 174 | if ( $.isFunction(this.onreadystatechange) ) { 175 | if( mockHandler.isTimeout) { 176 | this.status = -1; 177 | } 178 | this.onreadystatechange( mockHandler.isTimeout ? 'timeout' : undefined ); 179 | } else if ( mockHandler.isTimeout ) { 180 | // Fix for 1.3.2 timeout to keep success from firing. 181 | this.status = -1; 182 | } 183 | }).apply(that); 184 | }; 185 | })(this); 186 | 187 | if ( mockHandler.proxy ) { 188 | // We're proxying this request and loading in an external file instead 189 | _ajax({ 190 | global: false, 191 | url: mockHandler.proxy, 192 | type: mockHandler.proxyType, 193 | data: mockHandler.data, 194 | dataType: requestSettings.dataType === "script" ? "text/plain" : requestSettings.dataType, 195 | complete: function(xhr, txt) { 196 | mockHandler.responseXML = xhr.responseXML; 197 | mockHandler.responseText = xhr.responseText; 198 | mockHandler.status = xhr.status; 199 | mockHandler.statusText = xhr.statusText; 200 | this.responseTimer = setTimeout(process, mockHandler.responseTime || 0); 201 | } 202 | }); 203 | } else { 204 | // type == 'POST' || 'GET' || 'DELETE' 205 | if ( requestSettings.async === false ) { 206 | // TODO: Blocking delay 207 | process(); 208 | } else { 209 | this.responseTimer = setTimeout(process, mockHandler.responseTime || 50); 210 | } 211 | } 212 | } 213 | 214 | // Construct a mocked XHR Object 215 | function xhr(mockHandler, requestSettings, origSettings, origHandler) { 216 | // Extend with our default mockjax settings 217 | mockHandler = $.extend({}, $.mockjaxSettings, mockHandler); 218 | 219 | if (typeof mockHandler.headers === 'undefined') { 220 | mockHandler.headers = {}; 221 | } 222 | if ( mockHandler.contentType ) { 223 | mockHandler.headers['content-type'] = mockHandler.contentType; 224 | } 225 | 226 | return { 227 | status: mockHandler.status, 228 | statusText: mockHandler.statusText, 229 | readyState: 1, 230 | open: function() { }, 231 | send: function() { 232 | origHandler.fired = true; 233 | _xhrSend.call(this, mockHandler, requestSettings, origSettings); 234 | }, 235 | abort: function() { 236 | clearTimeout(this.responseTimer); 237 | }, 238 | setRequestHeader: function(header, value) { 239 | mockHandler.headers[header] = value; 240 | }, 241 | getResponseHeader: function(header) { 242 | // 'Last-modified', 'Etag', 'content-type' are all checked by jQuery 243 | if ( mockHandler.headers && mockHandler.headers[header] ) { 244 | // Return arbitrary headers 245 | return mockHandler.headers[header]; 246 | } else if ( header.toLowerCase() == 'last-modified' ) { 247 | return mockHandler.lastModified || (new Date()).toString(); 248 | } else if ( header.toLowerCase() == 'etag' ) { 249 | return mockHandler.etag || ''; 250 | } else if ( header.toLowerCase() == 'content-type' ) { 251 | return mockHandler.contentType || 'text/plain'; 252 | } 253 | }, 254 | getAllResponseHeaders: function() { 255 | var headers = ''; 256 | $.each(mockHandler.headers, function(k, v) { 257 | headers += k + ': ' + v + "\n"; 258 | }); 259 | return headers; 260 | } 261 | }; 262 | } 263 | 264 | // Process a JSONP mock request. 265 | function processJsonpMock( requestSettings, mockHandler, origSettings ) { 266 | // Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here 267 | // because there isn't an easy hook for the cross domain script tag of jsonp 268 | 269 | processJsonpUrl( requestSettings ); 270 | 271 | requestSettings.dataType = "json"; 272 | if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) { 273 | createJsonpCallback(requestSettings, mockHandler); 274 | 275 | // We need to make sure 276 | // that a JSONP style response is executed properly 277 | 278 | var rurl = /^(\w+:)?\/\/([^\/?#]+)/, 279 | parts = rurl.exec( requestSettings.url ), 280 | remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host); 281 | 282 | requestSettings.dataType = "script"; 283 | if(requestSettings.type.toUpperCase() === "GET" && remote ) { 284 | var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings ); 285 | 286 | // Check if we are supposed to return a Deferred back to the mock call, or just 287 | // signal success 288 | if(newMockReturn) { 289 | return newMockReturn; 290 | } else { 291 | return true; 292 | } 293 | } 294 | } 295 | return null; 296 | } 297 | 298 | // Append the required callback parameter to the end of the request URL, for a JSONP request 299 | function processJsonpUrl( requestSettings ) { 300 | if ( requestSettings.type.toUpperCase() === "GET" ) { 301 | if ( !CALLBACK_REGEX.test( requestSettings.url ) ) { 302 | requestSettings.url += (/\?/.test( requestSettings.url ) ? "&" : "?") + 303 | (requestSettings.jsonp || "callback") + "=?"; 304 | } 305 | } else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) { 306 | requestSettings.data = (requestSettings.data ? requestSettings.data + "&" : "") + (requestSettings.jsonp || "callback") + "=?"; 307 | } 308 | } 309 | 310 | // Process a JSONP request by evaluating the mocked response text 311 | function processJsonpRequest( requestSettings, mockHandler, origSettings ) { 312 | // Synthesize the mock request for adding a script tag 313 | var callbackContext = origSettings && origSettings.context || requestSettings, 314 | newMock = null; 315 | 316 | 317 | // If the response handler on the moock is a function, call it 318 | if ( mockHandler.response && $.isFunction(mockHandler.response) ) { 319 | mockHandler.response(origSettings); 320 | } else { 321 | 322 | // Evaluate the responseText javascript in a global context 323 | if( typeof mockHandler.responseText === 'object' ) { 324 | $.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')'); 325 | } else { 326 | $.globalEval( '(' + mockHandler.responseText + ')'); 327 | } 328 | } 329 | 330 | // Successful response 331 | jsonpSuccess( requestSettings, mockHandler ); 332 | jsonpComplete( requestSettings, mockHandler ); 333 | 334 | // If we are running under jQuery 1.5+, return a deferred object 335 | if(jQuery.Deferred){ 336 | newMock = new jQuery.Deferred(); 337 | if(typeof mockHandler.responseText == "object"){ 338 | newMock.resolveWith( callbackContext, [mockHandler.responseText] ); 339 | } 340 | else{ 341 | newMock.resolveWith( callbackContext, [jQuery.parseJSON( mockHandler.responseText )] ); 342 | } 343 | } 344 | return newMock; 345 | } 346 | 347 | 348 | // Create the required JSONP callback function for the request 349 | function createJsonpCallback( requestSettings, mockHandler ) { 350 | jsonp = requestSettings.jsonpCallback || ("jsonp" + jsc++); 351 | 352 | // Replace the =? sequence both in the query string and the data 353 | if ( requestSettings.data ) { 354 | requestSettings.data = (requestSettings.data + "").replace(CALLBACK_REGEX, "=" + jsonp + "$1"); 355 | } 356 | 357 | requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, "=" + jsonp + "$1"); 358 | 359 | 360 | // Handle JSONP-style loading 361 | window[ jsonp ] = window[ jsonp ] || function( tmp ) { 362 | data = tmp; 363 | jsonpSuccess( requestSettings, mockHandler ); 364 | jsonpComplete( requestSettings, mockHandler ); 365 | // Garbage collect 366 | window[ jsonp ] = undefined; 367 | 368 | try { 369 | delete window[ jsonp ]; 370 | } catch(e) {} 371 | 372 | if ( head ) { 373 | head.removeChild( script ); 374 | } 375 | }; 376 | } 377 | 378 | // The JSONP request was successful 379 | function jsonpSuccess(requestSettings, mockHandler) { 380 | // If a local callback was specified, fire it and pass it the data 381 | if ( requestSettings.success ) { 382 | requestSettings.success.call( callbackContext, ( mockHandler.response ? mockHandler.response.toString() : mockHandler.responseText || ''), status, {} ); 383 | } 384 | 385 | // Fire the global callback 386 | if ( requestSettings.global ) { 387 | trigger(requestSettings, "ajaxSuccess", [{}, requestSettings] ); 388 | } 389 | } 390 | 391 | // The JSONP request was completed 392 | function jsonpComplete(requestSettings, mockHandler) { 393 | // Process result 394 | if ( requestSettings.complete ) { 395 | requestSettings.complete.call( callbackContext, {} , status ); 396 | } 397 | 398 | // The request was completed 399 | if ( requestSettings.global ) { 400 | trigger( "ajaxComplete", [{}, requestSettings] ); 401 | } 402 | 403 | // Handle the global AJAX counter 404 | if ( requestSettings.global && ! --jQuery.active ) { 405 | jQuery.event.trigger( "ajaxStop" ); 406 | } 407 | } 408 | 409 | 410 | // The core $.ajax replacement. 411 | function handleAjax( url, origSettings ) { 412 | var mockRequest, requestSettings, mockHandler; 413 | 414 | // If url is an object, simulate pre-1.5 signature 415 | if ( typeof url === "object" ) { 416 | origSettings = url; 417 | url = undefined; 418 | } else { 419 | // work around to support 1.5 signature 420 | origSettings.url = url; 421 | } 422 | 423 | // Extend the original settings for the request 424 | requestSettings = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings); 425 | 426 | // Iterate over our mock handlers (in registration order) until we find 427 | // one that is willing to intercept the request 428 | for(var k = 0; k < mockHandlers.length; k++) { 429 | if ( !mockHandlers[k] ) { 430 | continue; 431 | } 432 | 433 | mockHandler = getMockForRequest( mockHandlers[k], requestSettings ); 434 | if(!mockHandler) { 435 | // No valid mock found for this request 436 | continue; 437 | } 438 | 439 | // Handle console logging 440 | logMock( mockHandler, requestSettings ); 441 | 442 | 443 | if ( requestSettings.dataType === "jsonp" ) { 444 | if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) { 445 | // This mock will handle the JSONP request 446 | return mockRequest; 447 | } 448 | } 449 | 450 | 451 | // Removed to fix #54 - keep the mocking data object intact 452 | //mockHandler.data = requestSettings.data; 453 | 454 | mockHandler.cache = requestSettings.cache; 455 | mockHandler.timeout = requestSettings.timeout; 456 | mockHandler.global = requestSettings.global; 457 | 458 | (function(mockHandler, requestSettings, origSettings, origHandler) { 459 | mockRequest = _ajax.call($, $.extend(true, {}, origSettings, { 460 | // Mock the XHR object 461 | xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ) } 462 | })); 463 | })(mockHandler, requestSettings, origSettings, mockHandlers[k]); 464 | 465 | return mockRequest; 466 | } 467 | 468 | // We don't have a mock request, trigger a normal request 469 | return _ajax.apply($, [origSettings]); 470 | } 471 | 472 | 473 | // Public 474 | 475 | $.extend({ 476 | ajax: handleAjax 477 | }); 478 | 479 | $.mockjaxSettings = { 480 | //url: null, 481 | //type: 'GET', 482 | log: function(msg) { 483 | window['console'] && window.console.log && window.console.log(msg); 484 | }, 485 | status: 200, 486 | statusText: "OK", 487 | responseTime: 500, 488 | isTimeout: false, 489 | contentType: 'text/plain', 490 | response: '', 491 | responseText: '', 492 | responseXML: '', 493 | proxy: '', 494 | proxyType: 'GET', 495 | 496 | lastModified: null, 497 | etag: '', 498 | headers: { 499 | etag: 'IJF@H#@923uf8023hFO@I#H#', 500 | 'content-type' : 'text/plain' 501 | } 502 | }; 503 | 504 | $.mockjax = function(settings) { 505 | var i = mockHandlers.length; 506 | mockHandlers[i] = settings; 507 | return i; 508 | }; 509 | $.mockjaxClear = function(i) { 510 | if ( arguments.length == 1 ) { 511 | mockHandlers[i] = null; 512 | } else { 513 | mockHandlers = []; 514 | } 515 | }; 516 | $.mockjax.handler = function(i) { 517 | if ( arguments.length == 1 ) { 518 | return mockHandlers[i]; 519 | } 520 | }; 521 | })(jQuery); 522 | 523 | -------------------------------------------------------------------------------- /lib/Bacon.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Bacon, Bus, Dispatcher, End, Error, Event, EventStream, Initial, Next, None, Observable, Property, PropertyDispatcher, Some, assert, assertArray, assertEvent, assertFunction, assertString, cloneArray, cloneObject, end, former, initial, isEvent, isFieldKey, isFunction, latter, makeFunction, methodCall, next, nop, partiallyApplied, propertyThenStream, remove, sendWrapped, toCombinator, toEvent, toFieldExtractor, toFieldKey, toOption, toSimpleExtractor, _, _ref, 3 | __slice = Array.prototype.slice, 4 | __hasProp = Object.prototype.hasOwnProperty, 5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; }, 6 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 7 | 8 | if ((_ref = this.jQuery || this.Zepto) != null) { 9 | _ref.fn.asEventStream = function(eventName, selector, eventTransformer) { 10 | var element; 11 | if (eventTransformer == null) eventTransformer = _.id; 12 | if (isFunction(selector)) { 13 | eventTransformer = selector; 14 | selector = null; 15 | } 16 | element = this; 17 | return new EventStream(function(sink) { 18 | var handler, unbind; 19 | handler = function() { 20 | var args, reply; 21 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 22 | reply = sink(next(eventTransformer.apply(null, args))); 23 | if (reply === Bacon.noMore) return unbind(); 24 | }; 25 | unbind = function() { 26 | return element.off(eventName, selector, handler); 27 | }; 28 | element.on(eventName, selector, handler); 29 | return unbind; 30 | }); 31 | }; 32 | } 33 | 34 | Bacon = this.Bacon = {}; 35 | 36 | Bacon.fromPromise = function(promise) { 37 | return new Bacon.EventStream(function(sink) { 38 | var onError, onSuccess; 39 | onSuccess = function(value) { 40 | sink(new Next(value)); 41 | return sink(new End); 42 | }; 43 | onError = function(e) { 44 | sink(new Error(e)); 45 | return sink(new End); 46 | }; 47 | promise.then(onSuccess, onError); 48 | return nop; 49 | }); 50 | }; 51 | 52 | Bacon.noMore = "veggies"; 53 | 54 | Bacon.more = "moar bacon!"; 55 | 56 | Bacon.later = function(delay, value) { 57 | return Bacon.sequentially(delay, [value]); 58 | }; 59 | 60 | Bacon.sequentially = function(delay, values) { 61 | var index, poll; 62 | index = -1; 63 | poll = function() { 64 | index++; 65 | if (index < values.length) { 66 | return toEvent(values[index]); 67 | } else { 68 | return end(); 69 | } 70 | }; 71 | return Bacon.fromPoll(delay, poll); 72 | }; 73 | 74 | Bacon.repeatedly = function(delay, values) { 75 | var index, poll; 76 | index = -1; 77 | poll = function() { 78 | index++; 79 | return toEvent(values[index % values.length]); 80 | }; 81 | return Bacon.fromPoll(delay, poll); 82 | }; 83 | 84 | Bacon.fromCallback = function() { 85 | var args, f; 86 | f = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 87 | f = makeFunction(f, args); 88 | return new EventStream(function(sink) { 89 | var handler; 90 | handler = function(value) { 91 | sink(next(value)); 92 | return sink(end()); 93 | }; 94 | f(handler); 95 | return nop; 96 | }); 97 | }; 98 | 99 | Bacon.fromPoll = function(delay, poll) { 100 | return new EventStream(function(sink) { 101 | var handler, id, unbind; 102 | id = void 0; 103 | handler = function() { 104 | var reply, value; 105 | value = poll(); 106 | reply = sink(value); 107 | if (reply === Bacon.noMore || value.isEnd()) return unbind(); 108 | }; 109 | unbind = function() { 110 | return clearInterval(id); 111 | }; 112 | id = setInterval(handler, delay); 113 | return unbind; 114 | }); 115 | }; 116 | 117 | Bacon.fromEventTarget = function(target, eventName) { 118 | return new EventStream(function(sink) { 119 | var handler, unbind; 120 | handler = function(event) { 121 | var reply; 122 | reply = sink(next(event)); 123 | if (reply === Bacon.noMore) return unbind(); 124 | }; 125 | if (target.addEventListener) { 126 | unbind = function() { 127 | return target.removeEventListener(eventName, handler, false); 128 | }; 129 | target.addEventListener(eventName, handler, false); 130 | } else { 131 | unbind = function() { 132 | return target.removeListener(eventName, handler); 133 | }; 134 | target.addListener(eventName, handler); 135 | } 136 | return unbind; 137 | }); 138 | }; 139 | 140 | Bacon.interval = function(delay, value) { 141 | var poll; 142 | if (value == null) value = {}; 143 | poll = function() { 144 | return next(value); 145 | }; 146 | return Bacon.fromPoll(delay, poll); 147 | }; 148 | 149 | Bacon.constant = function(value) { 150 | return new Property(sendWrapped([value], initial)); 151 | }; 152 | 153 | Bacon.never = function() { 154 | return Bacon.fromArray([]); 155 | }; 156 | 157 | Bacon.once = function(value) { 158 | return Bacon.fromArray([value]); 159 | }; 160 | 161 | Bacon.fromArray = function(values) { 162 | return new EventStream(sendWrapped(values, next)); 163 | }; 164 | 165 | sendWrapped = function(values, wrapper) { 166 | return function(sink) { 167 | var value, _i, _len; 168 | for (_i = 0, _len = values.length; _i < _len; _i++) { 169 | value = values[_i]; 170 | sink(wrapper(value)); 171 | } 172 | sink(end()); 173 | return nop; 174 | }; 175 | }; 176 | 177 | Bacon.combineAll = function(streams, f) { 178 | var next, stream, _i, _len, _ref2; 179 | assertArray(streams); 180 | stream = _.head(streams); 181 | _ref2 = _.tail(streams); 182 | for (_i = 0, _len = _ref2.length; _i < _len; _i++) { 183 | next = _ref2[_i]; 184 | stream = f(stream, next); 185 | } 186 | return stream; 187 | }; 188 | 189 | Bacon.mergeAll = function(streams) { 190 | return Bacon.combineAll(streams, function(s1, s2) { 191 | return s1.merge(s2); 192 | }); 193 | }; 194 | 195 | Bacon.combineAsArray = function() { 196 | var more, next, stream, streams, _i, _len, _ref2; 197 | streams = arguments[0], more = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 198 | if (!(streams instanceof Array)) streams = [streams].concat(more); 199 | if (streams.length) { 200 | stream = (_.head(streams)).toProperty().map(function(x) { 201 | return [x]; 202 | }); 203 | _ref2 = _.tail(streams); 204 | for (_i = 0, _len = _ref2.length; _i < _len; _i++) { 205 | next = _ref2[_i]; 206 | stream = stream.combine(next, function(xs, x) { 207 | return xs.concat([x]); 208 | }); 209 | } 210 | return stream; 211 | } else { 212 | return Bacon.constant([]); 213 | } 214 | }; 215 | 216 | Bacon.combineWith = function(streams, f) { 217 | return Bacon.combineAll(streams, function(s1, s2) { 218 | return s1.toProperty().combine(s2, f); 219 | }); 220 | }; 221 | 222 | Bacon.combineTemplate = function(template) { 223 | var applyStreamValue, combinator, compileTemplate, constantValue, current, funcs, setValue, streams; 224 | funcs = []; 225 | streams = []; 226 | current = function(ctxStack) { 227 | return ctxStack[ctxStack.length - 1]; 228 | }; 229 | setValue = function(ctxStack, key, value) { 230 | return current(ctxStack)[key] = value; 231 | }; 232 | applyStreamValue = function(key, index) { 233 | return function(ctxStack, values) { 234 | return setValue(ctxStack, key, values[index]); 235 | }; 236 | }; 237 | constantValue = function(key, value) { 238 | return function(ctxStack, values) { 239 | return setValue(ctxStack, key, value); 240 | }; 241 | }; 242 | compileTemplate = function(template) { 243 | var key, popContext, pushContext, value, _results; 244 | _results = []; 245 | for (key in template) { 246 | value = template[key]; 247 | if (value instanceof Observable) { 248 | streams.push(value); 249 | _results.push(funcs.push(applyStreamValue(key, streams.length - 1))); 250 | } else if (typeof value === "object") { 251 | pushContext = function(key) { 252 | return function(ctxStack, values) { 253 | var newContext; 254 | newContext = {}; 255 | setValue(ctxStack, key, newContext); 256 | return ctxStack.push(newContext); 257 | }; 258 | }; 259 | popContext = function(ctxStack, values) { 260 | return ctxStack.pop(); 261 | }; 262 | funcs.push(pushContext(key)); 263 | compileTemplate(value); 264 | _results.push(funcs.push(popContext)); 265 | } else { 266 | _results.push(funcs.push(constantValue(key, value))); 267 | } 268 | } 269 | return _results; 270 | }; 271 | compileTemplate(template); 272 | combinator = function(values) { 273 | var ctxStack, f, rootContext, _i, _len; 274 | rootContext = {}; 275 | ctxStack = [rootContext]; 276 | for (_i = 0, _len = funcs.length; _i < _len; _i++) { 277 | f = funcs[_i]; 278 | f(ctxStack, values); 279 | } 280 | return rootContext; 281 | }; 282 | return Bacon.combineAsArray(streams).map(combinator); 283 | }; 284 | 285 | Bacon.latestValue = function(src) { 286 | var latest, 287 | _this = this; 288 | latest = void 0; 289 | src.subscribe(function(event) { 290 | if (event.hasValue()) return latest = event.value; 291 | }); 292 | return function() { 293 | return latest; 294 | }; 295 | }; 296 | 297 | Event = (function() { 298 | 299 | function Event() {} 300 | 301 | Event.prototype.isEvent = function() { 302 | return true; 303 | }; 304 | 305 | Event.prototype.isEnd = function() { 306 | return false; 307 | }; 308 | 309 | Event.prototype.isInitial = function() { 310 | return false; 311 | }; 312 | 313 | Event.prototype.isNext = function() { 314 | return false; 315 | }; 316 | 317 | Event.prototype.isError = function() { 318 | return false; 319 | }; 320 | 321 | Event.prototype.hasValue = function() { 322 | return false; 323 | }; 324 | 325 | Event.prototype.filter = function(f) { 326 | return true; 327 | }; 328 | 329 | Event.prototype.getOriginalEvent = function() { 330 | if (this.sourceEvent != null) { 331 | return this.sourceEvent.getOriginalEvent(); 332 | } else { 333 | return this; 334 | } 335 | }; 336 | 337 | Event.prototype.onDone = function(listener) { 338 | return listener(); 339 | }; 340 | 341 | return Event; 342 | 343 | })(); 344 | 345 | Next = (function(_super) { 346 | 347 | __extends(Next, _super); 348 | 349 | function Next(value, sourceEvent) { 350 | this.value = value; 351 | } 352 | 353 | Next.prototype.isNext = function() { 354 | return true; 355 | }; 356 | 357 | Next.prototype.hasValue = function() { 358 | return true; 359 | }; 360 | 361 | Next.prototype.fmap = function(f) { 362 | return this.apply(f(this.value)); 363 | }; 364 | 365 | Next.prototype.apply = function(value) { 366 | return next(value, this.getOriginalEvent()); 367 | }; 368 | 369 | Next.prototype.filter = function(f) { 370 | return f(this.value); 371 | }; 372 | 373 | Next.prototype.describe = function() { 374 | return this.value; 375 | }; 376 | 377 | return Next; 378 | 379 | })(Event); 380 | 381 | Initial = (function(_super) { 382 | 383 | __extends(Initial, _super); 384 | 385 | function Initial() { 386 | Initial.__super__.constructor.apply(this, arguments); 387 | } 388 | 389 | Initial.prototype.isInitial = function() { 390 | return true; 391 | }; 392 | 393 | Initial.prototype.isNext = function() { 394 | return false; 395 | }; 396 | 397 | Initial.prototype.apply = function(value) { 398 | return initial(value, this.getOriginalEvent()); 399 | }; 400 | 401 | return Initial; 402 | 403 | })(Next); 404 | 405 | End = (function(_super) { 406 | 407 | __extends(End, _super); 408 | 409 | function End() { 410 | End.__super__.constructor.apply(this, arguments); 411 | } 412 | 413 | End.prototype.isEnd = function() { 414 | return true; 415 | }; 416 | 417 | End.prototype.fmap = function() { 418 | return this; 419 | }; 420 | 421 | End.prototype.apply = function() { 422 | return this; 423 | }; 424 | 425 | End.prototype.describe = function() { 426 | return "