Home Reference Source

lib/index.js

import Backbone from 'backbone'
import objectPath from './object-path'
import deepCopy from './deep-copy'

const DEFAULTS = Object.freeze({
  pathSeparator: '.',
  pathParser: null,
})

const defaults = Object.assign({}, DEFAULTS)

function updateObjectPath() {
  objectPath.pathSeparator = defaults.pathSeparator

  const { pathParser } = defaults
  if (typeof pathParser === 'function' || pathParser == null) {
    objectPath.pathParser = pathParser
  }

  return objectPath
}

function isObject(value) {
  return value != null && typeof value === 'object'
}

/**
 * @class
 * @see http://backbonejs.org/#Model
 *
 * @example
 * class Person extends DeepModel {...}
 *
 * // or
 * const Person = DeepModel.extend({...})
 */
export default class DeepModel extends Backbone.Model {
  /**
   * this module's version.
   */
  static get VERSION() {
    return '0.0.0'
  }

  /**
   * Update default settings.
   *
   * @param {Object} [settings={}] reset if `null`
   * @param {string} [settings.pathSeparator='/']
   * @param {function(path: string): Array.<string>} [settings.pathParser] ignore if returns `[]`
   * @returns {Object}
   *
   * @example
   * DeepModel.defaults({anySetting: true})
   * DeepModel.defaults(null) // reset!
   *
   * @example
   * DeepModel.defaults({pathSeparator: '/'})
   *
   * const model = new DeepModel()
   * model.set('a', {})
   * model.set('a/b', 1)
   * model.get('a/b') //=> 1
   *
   * @example
   * DeepModel.defaults({
   *   pathParser(path) {
   *     if (path === '*') { return [] } // ignore!
   *     return path.split('_')
   *   }
   * })
   *
   * const model = new DeepModel()
   * model.set('a', {})
   * model.set('a_b', 1)
   * model.get('a_b') //=> 1
   * model.set('*', 2)
   * model.get('*') //=> undefined
   */
  static defaults(settings = {}) {
    return Object.assign(defaults, settings == null ? DEFAULTS : settings)
  }

  /**
   * @see http://backbonejs.org/#Model-get
   * @override
   * @param {string} attribute
   * @returns {*}
   *
   * @example
   * model.get('a.b')
   */
  get(attribute) {
    return updateObjectPath().get(this.attributes, attribute)
  }

  /**
   * @see http://backbonejs.org/#Model-set
   * @override
   * @param {string} attribute
   * @param {*}      value
   * @param {Object} [options]
   * @returns {DeepModel}
   *
   * @example
   * model.set({'a.b': 'value'})
   * model.set('a.b', 'value')
   */
  set(attribute, value, options) {
    if (attribute == null) {
      return this
    }

    const _ = updateObjectPath()

    let attrs
    if (typeof attribute === 'object') {
      attrs = attribute
      options = value // eslint-disable-line no-param-reassign
    } else {
      if (!_.pathParser && !_.hasSeparator(attribute)) {
        return super.set(attribute, value, options)
      }
      attrs = {}
      attrs[attribute] = value
    }

    const newAttrs = Object.keys(attrs).reduce((newObj, path) => {
      const paths = _.parse(path)

      if (paths.length === 0) {
        return newObj
      }
      if (paths.length === 1) {
        newObj[paths[0]] = attrs[path] // eslint-disable-line no-param-reassign
        return newObj
      }

      const rootPath = paths[0]
      let obj = newObj[rootPath]
      if (obj == null) {
        obj = deepCopy(this.attributes[rootPath])
      }
      if (!isObject(obj)) {
        throw new Error(`"${path}" does not exist in ${JSON.stringify(this)}`)
      }
      newObj[rootPath] = obj // eslint-disable-line no-param-reassign

      return _.set(newObj, paths, attrs[path])
    }, {})

    return super.set(newAttrs, options)
  }
}

DeepModel.extend = Backbone.Model.extend