├── package.json ├── README.md └── sync.coffee /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone-hooksync", 3 | "version": "0.2.0", 4 | "main": "./sync", 5 | "dependencies": { 6 | "backbone": "", 7 | "underscore": "" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Backbone.HookSync 2 | 3 | Get the CRUD out of your app. Define functions for Backbone.Model create, read, update and delete. 4 | 5 | ```coffeescript 6 | class MyAwesomeModel extends Backbone.Model 7 | sync: Backbone.HookSync.make 8 | create: myAwesomeAPIsCreateMethod 9 | ``` 10 | 11 | ### More Details 12 | 13 | See the [Annotated Source](http://github.hubspot.com/Backbone.HookSync/docs/sync.html) 14 | -------------------------------------------------------------------------------- /sync.coffee: -------------------------------------------------------------------------------- 1 | # ### Backbone.HookSync 2 | # 3 | # This file provides a function, `make` for building a new 4 | # sync function for your Backbone.Model which will call the methods 5 | # you define. It is particularily useful for integrating Backbone 6 | # with the JavaScript API interface of your choice. 7 | # 8 | # The basic idea: 9 | # 10 | # class MyAwesomeModel extends Backbone.Model 11 | # sync: Backbone.HookSync.make 12 | # create: myAwesomeCreator 13 | # update: 'create' 14 | # delete: 'default' 15 | # read: 16 | # do: myAwesomeReader 17 | # build: (method, model, options) -> 18 | # model.attributes 19 | # 20 | # The file also provides two functions to add a new sync method to 21 | # your existing classes. 22 | # 23 | # `bind` adds the method to an existing class: 24 | # 25 | # class MyAwesomeModel extends Backbone.Model 26 | # # Model Dets Here 27 | # 28 | # Backbone.HookSync.bind MyAwesomeModel, 29 | # create: newCreator 30 | # 31 | # # Reads, Updates and Deletes will continue to use the 32 | # # models sync, or if one is not defined, Backbone.Sync 33 | # 34 | # `wrap` returns a new copy of your class with the sync method replaced: 35 | # 36 | # class MyAwesomeModel extends Backbone.Model 37 | # # Some Modeling.. 38 | # 39 | # MoreAwesomeModel = Backbone.HookSync.wrap MyAwesomeModel, 40 | # update: _.noop 41 | # 42 | # #### Options 43 | # 44 | # All three methods expect an object containing 1-4 of the CRUD methods: 45 | # 46 | # - create 47 | # - read 48 | # - update 49 | # - delete 50 | # 51 | # Optionally, a `sync` method which will be used with requests which 52 | # do not match one of the provided methods (or for methods defined as 53 | # 'default'). 54 | # 55 | # Optionally, a `defaults` object which provides defaults to the four 56 | # CRUD methods. 57 | # 58 | # Each of the CRUD method keys can have any of the following values: 59 | # 60 | # - A function (to do the action) 61 | # - A string referring to another CRUD method who's value should be used 62 | # - 'default', `null`, or `undefined` representing the default sync behavior 63 | # - An object 64 | # 65 | # If an object is used it can have the following attributes: 66 | # - `function do` - The function to be called (make sure you use string notation ["do"] if 67 | # you're not writing CoffeeScript) 68 | # - `function build(method, model, options)` - A function used to build the request passed 69 | # into do. Defaults to `model.toJSON()`. 70 | # - `boolean expandArguments[false]` - Should the array returned by `build` be 71 | # expanded and passed into do as seperate arguments? 72 | # - `boolean returnsPromise[false]` - Does do return a Deferred object? If so 73 | # it's done and fail methods will trigger the success and error 74 | # callbacks (and the default callbacks will be disabled). 75 | # - `boolean addOptions[true]` - Should the options hash be merged in with 76 | # the return value of build? 77 | # 78 | # If you're using expandArguments, addOptions: false is implied. 79 | # 80 | 81 | if module? and not window?.module? 82 | _ = require('underscore') 83 | Backbone = require('backbone') 84 | 85 | CRUD = ['create', 'read', 'update', 'delete'] 86 | 87 | HANDLER_DEFAULTS = 88 | addOptions: true 89 | 90 | # Returns a copy of the class with sync extended by the handlers. 91 | wrap = (cls, handlers) -> 92 | nCls = cls 93 | nCls:: = _.clone cls:: 94 | 95 | bind nCls, handlers 96 | 97 | nCls 98 | 99 | # Mutates an existing class to replace sync with a new sync 100 | # method powered by the handlers. 101 | # 102 | # Preserves a reference to the existing sync, so any method 103 | # not handled will fall through to the original. 104 | bind = (cls, handlers) -> 105 | if cls::sync 106 | # make knows to look at handlers.sync if the sync method 107 | # is not bound in the handlers (or is bound as 'default'). 108 | # 109 | # If there is no cls::sync (the default case for Backbone), 110 | # make will use Backbone.sync. 111 | handlers.sync ?= cls::sync 112 | 113 | cls::sync = make handlers 114 | 115 | cls 116 | 117 | # Build a sync function compatable with Backbone out of one or more 118 | # CRUD functions. Anything you don't override will get passed through 119 | # to the default sync function. 120 | make = (handlers) -> 121 | # Handlers is a map of CRUD methods to the functions which should 122 | # handle them + some other options. 123 | 124 | # Replace all of the string handler pointers with 125 | # the actual handlers they point to. Replace 'default' 126 | # with null 127 | resolveHandlers handlers 128 | 129 | # Normalize the handler to always be an object with a 130 | # make method. 131 | handlersToObjects handlers 132 | 133 | # `handlers.defaults` can contain default options for 134 | # all of the handlers 135 | applyDefaults handlers 136 | 137 | 138 | # This is the sync-replacement we'll be returning 139 | (method, model, options) -> 140 | handler = handlers[method] 141 | 142 | if handler 143 | request = buildRequest handler, method, model, options 144 | 145 | makeRequest handler, request, model, options 146 | 147 | else 148 | # This method is most likely gonna be overriding the 149 | # model's sync method, so calling @sync would recurse, so 150 | # we let the fall-through sync be passed in as an option. 151 | # 152 | # `wrap` and `bind` do this for you, automatically preserving the 153 | # previous sync in handlers. 154 | (handlers.sync or Backbone.sync) method, model, options 155 | 156 | # In this context, a request is the object which will be passed 157 | # into the `do` function passed in as a handler. Based on the 158 | # `expandArguments` property, it can either be a single object, 159 | # or an array of arguments. 160 | # 161 | # Unless you set handler.addOptions to false, the options object will be 162 | # automatically merged with the attributes your return. 163 | # 164 | # The build function defaults to `model.toJSON`. 165 | buildRequest = (handler, method, model, options) -> 166 | builder = handler.build ? model.toJSON 167 | 168 | req = builder.call model, method, model, options 169 | 170 | if handler.addOptions and not handler.expandArguments 171 | req = _.extend {}, options, req 172 | 173 | req 174 | 175 | # Do it! 176 | # 177 | # handler.returnsPromise gives you a convenient way to 178 | # convert a method which normally returns a promise to 179 | # work with Backbone's success and error handlers. 180 | # 181 | # It will replace the passed-in success and error handlers 182 | # with noops so they are not called twice. 183 | makeRequest = (handler, request, model, options) -> 184 | if handler.returnsPromise 185 | oldOptions = _.pick request, 'success', 'error' 186 | request.success = request.error = -> 187 | 188 | if handler.expandArguments 189 | resp = handler.do request... 190 | else 191 | resp = handler.do request, model, options 192 | 193 | if handler.returnsPromise 194 | resp.done (data) -> 195 | callSuccessCallback(model, data, options, oldOptions) 196 | model.trigger('sync', model, resp, options) 197 | 198 | resp.fail (err) -> 199 | oldOptions.error? err 200 | model.trigger('error', err, model, options) 201 | 202 | # After Backbone 0.9.2, the fetch success wrapper Backbone 203 | # creates changed the arguments supplied by the callback. 204 | # 205 | # While this isn't ideal, it's nice to be backwards compatible 206 | # to 0.9.2. 207 | callSuccessCallback = (model, data, options, oldOptions) -> 208 | if isBackboneVersionGreaterThan('0.9.2') and not isBackboneVersionGreaterThan('0.9.10') 209 | oldOptions.success? model, data, options 210 | else 211 | oldOptions.success? data 212 | 213 | # Handler can be passed in as either functions, or 214 | # objects which have some more options and functions. 215 | # To make it easier, lets make them objects all of the time. 216 | # 217 | # It modifies the passed in object in-place. 218 | handlersToObjects = (handlers) -> 219 | for type, handler of handlers when type in CRUD 220 | if _.isFunction handler 221 | handlers[type] = 222 | do: handler 223 | 224 | # Use handlers.defaults as the defaults for all of the other 225 | # handlers. 226 | # 227 | # Modifies the passed-in object in-place. 228 | applyDefaults = (handlers) -> 229 | for type, handler of handlers when type in CRUD 230 | handlers[type] = _.extend {}, HANDLER_DEFAULTS, handlers.defaults, handler 231 | 232 | # Handlers can be string references to the keys of other handlers, 233 | # or 'default'. 234 | # 235 | # Modifies the passed in object in-place. 236 | resolveHandlers = (handlers) -> 237 | for type, handler of handlers when type in CRUD 238 | switch handler 239 | when 'default' 240 | # In this context, `default` means pass request through to 241 | # `handlers.sync` or `Backbone.sync`. 242 | # 243 | # For convenience, normalize default to be falsy 244 | # as we also support simply skipping the handler type to 245 | # signify 'default' 246 | handlers[type] = null 247 | when 'create', 'update', 'read', 'delete' 248 | # You can alias the handler to another handler. 249 | # 250 | # The most common usage of this is to map create to the same thing 251 | # as update, e.g.: 252 | # 253 | # make 254 | # create: myHandler 255 | # update: 'create' 256 | # 257 | # This is only going to reliably work to one level of depth, 258 | # you can't reference references. 259 | handlers[type] = handlers[handler] 260 | 261 | isBackboneVersionGreaterThan = (compare) -> 262 | comparison = compare.split('.') 263 | current = Backbone.VERSION.split('.') 264 | 265 | parseInt(comparison[0]) < parseInt(current[0]) or 266 | parseInt(comparison[1]) < parseInt(current[1]) or 267 | parseInt(comparison[2]) < parseInt(current[2]) 268 | 269 | exports = {make, wrap, bind} 270 | Backbone?.HookSync = exports 271 | 272 | module?.exports = exports 273 | --------------------------------------------------------------------------------