Skip to content

Instantly share code, notes, and snippets.

@Skateside
Last active December 14, 2015 22:34
Show Gist options
  • Save Skateside/255a48439811b5b86f3d to your computer and use it in GitHub Desktop.
Save Skateside/255a48439811b5b86f3d to your computer and use it in GitHub Desktop.
Getter/Setter object in JavaScript. Loosely based on the Varien_Object in Magento
/**
* access
*
* Created by calling the `Access` function.
*
* var a1 = Access();
* var a2 = new Access();
*
* Initial data can be passed, see [[access.addData]].
*
* [[access]] objects have some pre-set methods but their main strength comes
* from the ability to access internal data using `get*`, `set*`, `has*` and
* `delete*` methods.
*
* For example:
*
* access.hasThing(); // -> false
* access.getThing(); // -> undefined
* access.setThing('thing'); // -> access
* access.hasThing(); // -> true
* access.getThing(); // -> "thing"
* access.deleteThing(); // -> true
* access.hasThing(); // -> false
*
* The internal data can be viewed using [[access.debug]], if needed.
**/
function Access(initial) {
'use strict';
var access = {},
internalData = {};
function decamelise(string) {
return util.String.hyphenate(util.String.toLowerFirst(string), '_');
}
/**
* access.getData(name) -> ?
* - name (String): Data property to retrieve.
**/
function getData(name) {
return internalData[name];
}
/**
* access.setData(name, value) -> access
* - name (String): Data property to set.
* - value (?): Value for the property.
**/
function setData(name, value) {
internalData[name] = value;
return access;
}
/**
* access.addData(data)
* - data (Object): Data to set.
*
* These two statements are equivalent.
*
* access.setData('one', 1).setData('two', 2);
* access.addData({one: 1, two: 2})
*
**/
function addData(data) {
util.Object.assign(internalData, data);
}
/**
* access.hasData(name) -> Boolean
* - name (String): Data property to check.
**/
function hasData(name) {
return util.Object.owns(internalData, name);
}
/**
* access.deleteData(name) -> Boolean
* - name (String): Data property to delete.
*
* Returns `true` if data was deleted and `false` otherwise.
**/
function deleteData(name) {
var had = hasData(name);
delete internalData[name];
return had;
}
/**
* access.debug() -> Object
*
* Returns a copy of the internal data to aid debugging.
**/
function debug() {
return util.Object.clone(internalData);
}
util.Object.assign(access, {
getData,
setData,
addData,
hasData,
deleteData,
debug
});
access = new Proxy(access, {
get: function (target, name) {
var value,
rule = name.match(/(^([a-z]+)(\w+))/),
property;
if (util.Object.owns(target, name)) {
value = target[name];
} else if (rule && rule.length) {
property = decamelise(rule[3]);
switch (rule[2]) {
case 'get':
target[name] = function () {
return target.getData(property);
};
break;
case 'set':
target[name] = function (val) {
return target.setData(property, val);
};
break;
case 'has':
target[name] = function () {
return target.hasData(property);
};
break;
case 'delete':
target[name] = function () {
return target.deleteData(property);
};
break;
}
value = target[name];
}
return value;
}
});
if (util.Object.isObject(initial)) {
addData(initial);
}
return access;
}
/**
* class InheritableAccess
*
* Variant of [[access]] that allows for inheritance. Magic `get`, `set`, `has`
* and `delete` methods are still available.
**/
var InheritableAccess = (function () {
'use strict';
var internalData = new WeakMap();
var getData = function (instance) {
if (!internalData.has(instance)) {
internalData.set(instance, {});
}
return internalData.get(instance);
};
var decamelise = function (string) {
return util.String.hyphenate(util.String.toLowerFirst(string), '_');
};
var InAccess = function (...args) {
return this.init(...args);
};
InAccess.prototype = {
/**
* new InheritableAccess([initial])
* - initial (Object): Optional initial data.
*
* Creates the access object. Initial data can optionally be set.
*
* var access1 = new InheritableAccess();
* access1.getSomething(); // -> undefined
* var access2 = new InheritableAccess({something: true});
* access2.getSomething(); // -> true
*
**/
init: function (initial) {
if (util.Object.isObject(initial)) {
this.addData(initial);
}
},
/**
* InheritableAccess#getData(key) -> ?
* - key (String): Data key.
*
* Returns data from the private data or `undefined` if no data can be
* found.
*
* var access = new InheritableAccess({something: true});
* access.getData('something'); // -> true
* access.getData('something_else'); // -> undefined
*
**/
getData: function (key) {
return getData(this)[key];
},
/**
* InheritableAccess#setData(key, value) -> InheritableAccess
* - key (String): Data key.
* - value (?): Data value.
*
* Sets internal data. The instance is returned so it can be chained.
*
* var access = new InheritableAccess();
* access.hasData('something'); // -> false
* access.setData('something', true); // -> access
* access.hasData('something'); // -> true
*
**/
setData: function (key, value) {
getData(this)[key] = value;
return this;
},
/**
* InheritableAccess#hasData(key) -> Boolean
* - key (String): Data key.
*
* Checks to see if the given `key` has any associated data.
*
* var access = new InheritableAccess();
* access.setData('something', true);
* access.setData('something_else', undefined);
* access.hasData('something'); // -> true
* access.hasData('something_else'); // -> true
* access.hasData('a_third_something'); // -> false
*
**/
hasData: function (key) {
return util.Object.owns(getData(this), key);
},
/**
* InheritableAccess#deleteData(key) -> Boolean
* - key (String): Data key.
*
* Deletes data from the private data. Returns `true` if data was
* removed and `false` otherwise.
*
* var access = new InheritableAccess();
* access.setData('something', true);
* access.deleteData('something'); // -> true
* access.deleteData('something_else'); // -> false
*
**/
deleteData: function (key) {
var had = this.hasData(key);
delete getData(this)[key];
return had;
},
/**
* InheritableAccess#addData(data)
* - data (Object): Data to add.
*
* Adds data to the instance.
*
* var access = new InheritableAccess();
* access.addData({something: true, something_else: false});
* access.getData('something'); // -> true
* access.getData('something_else'); // -> false
*
**/
addData: function (data) {
Object.keys(data).forEach(function (key) {
this.setData(key, data[key]);
}, this);
},
/**
* InheritableAccess#debug() -> Object
*
* Returns a copy of the private data to aid debugging. Although it
* should be possible to modify the copy without affecting the actual
* private data, this cannot be guarenteed.
*
* var access = new InheritableAccess();
* access.setData('something', true);
* access.debug(); // -> {something: true}
*
**/
debug: function () {
return util.Object.clone(getData(this));
}
};
InAccess.prototype = new Proxy(InAccess.prototype, {
get: function (target, name) {
var value,
rule = name.match(/(^([a-z]+)(\w+))/),
property;
if (util.Object.owns(target, name)) {
value = target[name];
} else if (rule && rule.length) {
property = decamelise(rule[3]);
switch (rule[2]) {
case 'get':
target[name] = function () {
return this.getData(property);
};
break;
case 'set':
target[name] = function (val) {
return this.setData(property, val);
};
break;
case 'has':
target[name] = function () {
return this.hasData(property);
};
break;
case 'delete':
target[name] = function () {
return this.deleteData(property);
};
break;
}
value = target[name];
}
return value;
}
});
return InAccess;
}());
/**
* util
*
* Utility functions.
**/
var util = (function () {
'use strict';
var utilities = {
/**
* util.Array
*
* Functions for manipulating Arrays
**/
Array: {},
/**
* util.Function
*
* Functions for manipulating Functions
**/
Function: {},
/**
* util.Number
*
* Functions.for manipulating Numbers.
**/
Number: {},
/**
* util.Object
*
* Functions for manipulating Objects.
**/
Object: {},
/**
* util.String
*
* Functions for manipulating Strings.
**/
String: {}
};
/**
* util.Object.assign(source, ...objects) -> Object
* - source (Object): Object to extend.
* - ...objects (Object): Objects to exetend with.
*
* Extends the `source` object with the other `objects`. Just a basic
* fallback for the native `Object.assign`.
**/
var assign = Object.assign || function (source, ...objects) {
objects.forEach(function (object) {
Object.keys(object).forEach(function (key) {
source[key] = object[key];
});
});
return source;
};
/**
* util.Function.identity(x) -> ?
* - x (?): Object to return.
*
* Returns an object without modifying it. This exists mainly as a
* fallback.
**/
var identity = function (x) {
return x;
};
/**
* util.Object.getClass(object) -> String
* - object (?): Object whose class should be returned.
*
* Returns the class of the object as defined in the ECMAScript specs.
*
* util.Object.getClass({}); // -> "Object"
* util.Object.getClass([]); // -> "Array"
* util.Object.getClass(''); // -> "String"
* util.Object.getClass(); // -> "Undefined"
*
**/
var getClass = function (object) {
var string = Object.prototype.toString.call(object);
return string.slice(8, -1);
};
/**
* util.Function.isFunction(func) -> Boolean
* - func (Function): Function to test.
*
* Checks to see if the given `func` is a function.
**/
var isFunction = function (func) {
return typeof func === 'function' && getClass(func) === 'Function';
};
/**
* util.Array.from(object) -> Array
* - object (?): Object to convert.
*
* Converts the given object into an array.
*
* var divs = document.querySelectorAll('div');
* // -> NodeList[<div id="one">, <div id="two">, <div id="three">]
* util.Array.from(divs);
* // -> Array[<div id="one">, <div id="two">, <div id="three">]
*
* Optionally, a `map` may be provided to convert the original object.
*
* util.Array.from(divs, function (div) {
* return div.id;
* });
* // -> Array['one', 'two', 'three']
*
**/
var arrayFrom = Array.from || function (array, map, context) {
if (!isFunction(map)) {
map = identity;
}
return Array.prototype.map.call(array, map, context);
};
/**
* util.Array.isArrayLike(object) -> Boolean
* - object (?): Object to test.
*
* Tests to see if the given object is array-like.
*
* util.Array.isArrayLike([]); // -> true
* util.Array.isArrayLike(''); // -> true
* util.Array.isArrayLike(0); // -> false
* util.Array.isArrayLike({}); // -> false
* util.Array.isArrayLike({length: 0}); // -> true
* util.Array.isArrayLike(document.querySelector('*')); // -> false
* util.Array.isArrayLike(document.querySelectorAll('*')); // -> true
*
**/
function isArrayLike(object) {
return object !== undefined && object !== null &&
isNumeric(object.length);
}
/**
* util.Object.isPlainObject(object) -> Boolean
* - object (?): Object to test.
*
* Test to see whether or not the given `object` is a plain object.
*
* util.Object.isPlainObject({}); // -> true
* util.Object.isPlainObject([]); // -> false
* util.Object.isPlainObject(null); // -> false
* util.Object.isPlainObject(document.body); // -> false
* util.Object.isPlainObject(window); // -> false
*
**/
function isPlainObject(object) {
var isPlain = object !== null && typeof object === 'object' &&
object !== window && !object.nodeType;
if (isPlain) {
try {
if (!object.constructor ||
!owns(object.constructor.prototype, 'isPrototypeOf')) {
isPlain = false;
}
} catch(ignore) {
}
}
return isPlain;
}
/**
* util.Object.owns(object, property) -> Boolean
* - object (Object): Object to test.
* - property (String): Property to check.
*
* Tests whether an object has the given property.
**/
function owns(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
}
/**
* util.Object.clone(object) -> Object
* - object (Object): Object to clone.
*
* Creates a clone of an object such that modifying the close should not
* modify the original. Some attempts are made to clone deeply.
**/
function clone(source) {
var copy = {};
Object.getOwnPropertyNames(source).forEach(function (property) {
var orig = source[property],
desc = Object.getOwnPropertyDescriptor(source, property),
value = (isArrayLike(orig) || isPlainObject(orig))
? clone(orig)
: orig;
Object.defineProperty(copy, property, assign(desc, {value}));
});
return isArrayLike(source)
? arrayFrom(copy)
: copy;
}
/**
* util.String.hyphenate(str[, hyphen = "-"]) -> String
* - str (String): String to hyphenate.
* - hyphen (String): Hyphen character.
*
* Converts a camelCase string into a hyphenated one.
*
* util.String.hyphenate('fontFamily'); // -> "font-family"
*
* The hyphen character can be defined to allow for a different
* substitution. If ommitted, or a string is not provided, a hyphen (`-`)
* is assumed.
*
* util.String.hyphenate('fontFamily', '='); // -> "font=family"
* util.String.hyphenate('fontFamily', 4); // -> "font-family"
* util.String.hyphenate('fontFamily', '---'); // -> "font---family"
*
**/
function hyphenate(str, hyphen) {
if (typeof hyphen !== 'string') {
hyphen = '-';
}
return str.replace(/([a-z])([A-Z])/g, function (ignore, lower, upper) {
return lower + hyphen + upper.toLowerCase();
});
}
/**
* util.String.toLowerFirst(str) -> String
* - str (String): String to convert.
*
* Converts a string so that the first character is lower case.
*
* util.String.toLowerFirst('Abc'); // -> "abc"
* util.String.toLowerFirst('abc'); // -> "abc"
* util.String.toLowerFirst('ABC'); // -> "aBC"
*
**/
function toLowerFirst(str) {
var string = String(str);
return string.charAt(0).toLowerCase() + string.slice(1);
}
assign(utilities.Array, {
from: arrayFrom,
isArrayLike
});
assign(utilities.Function, {
identity,
isFunction
});
assign(utilities.Number, {
isNumeric
});
assign(utilities.Object, {
assign,
clone,
getClass,
isObject,
owns
});
assign(utilities.String, {
hyphenate,
toLowerFirst
});
return utilities;
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment