schmackbone

jQuery-less, Promise-interfaced models based on BackboneJS

Usage no npm install needed!

<script type="module">
  import schmackbone from 'https://cdn.skypack.dev/schmackbone';
</script>

README

                       __      __
                      /\ \    /\ \                                   __
           __      ___\ \ \/'\\ \ \____    ___     ___      __      /\_\    ____
         /'__`\   /'___\ \ , < \ \ '__`\  / __`\ /' _ `\  /'__`\    \/\ \  /',__\
   SCHM-/\ \ \.\_/\ \__/\ \ \\`\\ \ \ \ \/\ \ \ \/\ \/\ \/\  __/  __ \ \ \/\__, `\
        \ \__/.\_\ \____\\ \_\ \_\ \_,__/\ \____/\ \_\ \_\ \____\/\_\_\ \ \/\____/
         \/__/\/_/\/____/ \/_/\/_/\/___/  \/___/  \/_/\/_/\/____/\/_/\ \_\ \/___/
                                                                    \ \____/
                                                                     \/___/
 (_'______________________________________________________________________________'_)
 (_.——————————————————————————————————————————————————————————————————————————————._)

Schmackbone.js

Schmackbone is a lighter, modernized fork of the established MV-library Backbone, with the View-logic, jQuery, and Router removed along with its support for legacy browsers. So it's really just an M-library to handle your RESTful API requests.

Its ajax requests are now native and promise-based, and its components are modularized so you can use what you need and tree-shake the rest.

Why?

While creating a Backbone view layer feels a little 2012, its Models and Collections are actually a very light and easy abstraction for interacting with REST APIs. Additionally, its basic Events module make it a cinch to pub/sub your model changes to your actual view layer, for example, your React Components. This allows for some really elegant abstractions without the heaviness of a full-blown state manager like Redux. You can keep your UI-state local and your data-state (the 'state' of the data that is represented in your API).

This is how the resourcerer library employs Schmackbone.

Practical Differences to Backbone

Underscore methods are now opt-in

Backbone automatically adds the bulk of the underscore library methods to its Model/Collection prototypes. But nearly all of them are unnecessary these days. Schmackbone leaves these out by default. However, if you want to add these back, just import the add_underscore_methods script to your application:

// your-app.js
import 'schmackbone/add_underscore_methods';

(Note that in order to access the add_underscore_methods script, you'll need a bundler (or other module loading system) that recognizes the exports property in package.json. For webpack, this means upgrading to v5.)

Modularized

// before, with webpack shimming via https://github.com/webpack-contrib/imports-loader:
import Backbone from 'backbone';

// after:
import * as Schmackbone from 'schmackbone';
// or
import {Model} from 'schmackbone';

Model and Collection are native Javascript classes

// before:
var MyModel = Model.extend({url: () => '/foo'});

// after:
class MyModel extends Model {
  url = () => '/foo'
}

Reserved instance properties now static properties

Due to the way instance properties are instantiated in subclasses (not until after the superclass has been instantiated), many of the reserved instance properties in Schmackbone have been moved to static properties:

// before:
var MyModel = Model.extend({
  defaults: {
    one: 1,
    two: 2
  },
  
  cidPrefix: 'm',
  
  idAttribute: '_id',
  
  url: () => '/foo'
});


// after:
class MyModel extends Model {
  static defaults = {
    one: 1,
    two: 2
  }
  
  static cidPrefix = 'm'
  
  static idAttribute = '_id'
  
  url() => '/foo'
});

Notes:

  • For Models, defaults, idAttribute, and cidPrefix are static properties. defaults can optionally be a static function.
  • For Collections, model and comparator properties are now static, but if they need to be overridden in an instance, they can do so via the options object in instantiation:
class MyCollection extends Collection {
  static comparator = 'created_at'
  static model = MyModel
}

const overrideCollection = new MyCollection([], {comparator: 'updated_at', model: MyModel2});
  • url (Model/Collection) and urlRoot (Model) remain instance properties, as they (1) often depend on the instance and (2) are not utilized during instantiation

Requests have a Promise interface

All Schmackbone request methods use window.fetch under the hood and so now have a Promise interface, instead of accepting jQuery-style success and error options.

// before:
todoModel.save({name: 'Giving This Todo A New Name'}, {
  success: (model, response, options) => notify('Todo save succeeded!'),
  error: (model, response, options) => notify('Todo save failed :/'),
  complete: () => saveAttempts = saveAttempts + 1
});

// after
todoModel.save({name: 'Giving This Todo A New Name'})
    .then(([model, response, options]) => notify('Todo save succeeded!'))
    .catch(([model, response, options]) => notify('Todo save failed :/'))
    .then(() => saveAttempts = saveAttempts + 1);

Note a couple important consequences:

  1. Since Promises can only resolve a single value, the callback parameters are passed via an array that can be destructured.
  2. All requests must have a .catch attached, even if the rejection is swallowed. Omitting one risks an uncaught Promise rejection exception if the request fails.
  3. The .create method no longer returns the added model; it returns the promise instead.

qs Dependency

While jQuery was no longer necessary, we could not replace it entirely with native javascript: we added query string stringifying functionality via the small-footprint qs library.

setAjaxPrefilter

Schmackbone offers one hook into its fetch requests: setAjaxPrefilter. It allows you to alter the options object passed to window.fetch before any requests are made. Use this hook to pass custom headers like auth headers, or a custom global error handler:

import {setAjaxPrefilter} from 'schmackbone';

// usage:
// @param {object} options object
// @return {object} modified options object
const ajaxPrefilter = (options={}) => ({
  ...options,
  // if you want to default all api requests to json
  contentType: 'application/json',
  error: (response) => {
    if (response.status === 401) {
      // refresh auth token logic
    } else if (response.status === 429) {
      // do some rate-limiting retry logic
    }

    return options.error(response);
  },
  headers: {
    ...options.headers,
    Authorization: `Bearer ${localStorage.getItem('super-secret-auth-token')}`
  }
});

setAjaxPrefilter(ajaxPrefilter);

By default, the ajaxPrefilter function is set to the identity function.

Misc

  • Note that Schmackbone uses ES2015 in its source and does no transpiling—including import/export (Local babel configuration is for testing, only). Unlike Backbone, whose legacy monolith used UMD syntax and could be used directly in the browser, Schmackbone can be used only in modern browsers via the type="module" script MIME-type or via a bundler like webpack that handles module transforms.

This means that if you're not babelifying your node_modules folder, you'll need to make an exception for this package, ie:

// webpack.config.js
module: {
  rules: [{
    test: /\.jsx?$/,
    exclude: /node_modules\/(?!(schmackbone))/,
    use: {loader: 'babel-loader?cacheDirectory=true'}
  }]
}