API Docs for: v2.11.1
Show:

File: packages/ember-metal/lib/property_set.js

import { toString } from 'ember-utils';
import { assert } from './debug';
import isEnabled from './features';
import { _getPath as getPath } from './property_get';
import {
  propertyWillChange,
  propertyDidChange
} from './property_events';

import EmberError from './error';
import {
  isPath,
  hasThis as pathHasThis
} from './path_cache';
import {
  peekMeta
} from './meta';

/**
  Sets the value of a property on an object, respecting computed properties
  and notifying observers and other listeners of the change. If the
  property is not defined but the object implements the `setUnknownProperty`
  method then that will be invoked as well.

  ```javascript
  Ember.set(obj, "name", value);
  ```

  @method set
  @for Ember
  @param {Object} obj The object to modify.
  @param {String} keyName The property key to set
  @param {Object} value The value to set
  @return {Object} the passed value.
  @public
*/
export function set(obj, keyName, value, tolerant) {
  assert(
    `Set must be called with three or four arguments; an object, a property key, a value and tolerant true/false`,
    arguments.length === 3 || arguments.length === 4
  );
  assert(`Cannot call set with '${keyName}' on an undefined object.`, obj && typeof obj === 'object' || typeof obj === 'function');
  assert(`The key provided to set must be a string, you passed ${keyName}`, typeof keyName === 'string');
  assert(`'this' in paths is not supported`, !pathHasThis(keyName));
  assert(`calling set on destroyed object: ${toString(obj)}.${keyName} = ${toString(value)}`, !obj.isDestroyed);

  if (isPath(keyName)) {
    return setPath(obj, keyName, value, tolerant);
  }

  let meta = peekMeta(obj);
  let possibleDesc = obj[keyName];

  let desc, currentValue;
  if (possibleDesc !== null && typeof possibleDesc === 'object' && possibleDesc.isDescriptor) {
    desc = possibleDesc;
  } else {
    currentValue = possibleDesc;
  }

  if (desc) { /* computed property */
    desc.set(obj, keyName, value);
  } else if (obj.setUnknownProperty && currentValue === undefined && !(keyName in obj)) { /* unknown property */
    assert('setUnknownProperty must be a function', typeof obj.setUnknownProperty === 'function');
    obj.setUnknownProperty(keyName, value);
  } else if (currentValue === value) { /* no change */
    return value;
  } else {
    propertyWillChange(obj, keyName);

    if (isEnabled('mandatory-setter')) {
      setWithMandatorySetter(meta, obj, keyName, value);
    } else {
      obj[keyName] = value;
    }

    propertyDidChange(obj, keyName);
  }

  return value;
}

if (isEnabled('mandatory-setter')) {
  var setWithMandatorySetter = function(meta, obj, keyName, value) {
    if (meta && meta.peekWatching(keyName) > 0) {
      makeEnumerable(obj, keyName);
      meta.writeValue(obj, keyName, value);
    } else {
      obj[keyName] = value;
    }
  };

  var makeEnumerable = function(obj, key) {
    let desc = Object.getOwnPropertyDescriptor(obj, key);

    if (desc && desc.set && desc.set.isMandatorySetter) {
      desc.enumerable = true;
      Object.defineProperty(obj, key, desc);
    }
  };
}

function setPath(root, path, value, tolerant) {
  // get the last part of the path
  let keyName = path.slice(path.lastIndexOf('.') + 1);

  // get the first part of the part
  path = (path === keyName) ? keyName : path.slice(0, path.length - (keyName.length + 1));

  // unless the path is this, look up the first part to
  // get the root
  if (path !== 'this') {
    root = getPath(root, path);
  }

  if (!keyName || keyName.length === 0) {
    throw new EmberError('Property set failed: You passed an empty path');
  }

  if (!root) {
    if (tolerant) {
      return;
    } else {
      throw new EmberError('Property set failed: object in path "' + path + '" could not be found or was destroyed.');
    }
  }

  return set(root, keyName, value);
}

/**
  Error-tolerant form of `Ember.set`. Will not blow up if any part of the
  chain is `undefined`, `null`, or destroyed.

  This is primarily used when syncing bindings, which may try to update after
  an object has been destroyed.

  @method trySet
  @for Ember
  @param {Object} root The object to modify.
  @param {String} path The property path to set
  @param {Object} value The value to set
  @public
*/
export function trySet(root, path, value) {
  return set(root, path, value, true);
}