lively.classes
Version:
EcmaScript 6 classes for live development
276 lines (238 loc) • 10.6 kB
JavaScript
// compactness
// types -> fabrik
// serialization, obscure references
// debugging, more usful inspector
// change system -> synchronization, serialization, debugging
// initialization order, dependencies (ex. btn => label => submoprhs needed)
// declaratively configuring objects
// propertySettings: {
// valueStoreProperty: STRING|SYMBOL - optional, defaults to _state. This is where the
// actual values of the properties will be stored by default
// defaultGetter: FUNCTION(STRING) - default getter to be used
// defaultSetter: FUNCTION(STRING, VALUE) - default setter to be used
// }
//
// ????????????
// propertyDescriptorCacheKey: STRING|SYMBOL - where the result of
// initializeProperties() should go
// ????????????
// properties:
// {STRING: DESCRIPTOR, ...}
// properties are merged in the proto chain
//
// descriptor: {
// get: FUNCTION - optional
// set: FUNCTION - optional
// defaultValue: OBJECT - optional
// initialize: FUNCTION - optional, function that when present should
// produce a value for the property. Run after object creation
// autoSetter: BOOL - optional, true if not specified
// usePropertyStore: BOOL - optional, true if not specified.
// priority: NUMBER - optional, true if not specified.
// before: [STRING] - optional, list of property names that depend on
// the descriptor's property and that should be
// initialized / sorted / ... *after*
// it. Think of it as a constraint: "this property
// needs to run before that property"
// after: [STRING] - optional, list of property names that this property depends on
// internal: BOOL - optional, if specified marks property as meant for
// internal housekeeping. At this point this is only used
// documentation and debugging purposes, it won't affect
// how the property works
// readOnly: BOOL - optional, if set prevents that the property is mutated. Also
// prevents the serializer from storing that value state.
// }
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
import { obj} from "lively.lang";
import { getClassHierarchy } from "./runtime.js";
const defaultPropertiesSettingKey = "propertySettings",
defaultPropertiesKey = "properties",
defaultInstanceInitializerMethod = "initializeProperties",
propertiesAndSettingsCacheSym = Symbol.for("lively.classes-properties-and-settings");
const defaultPropertySettings = {
defaultSetter: null,
defaultGetter: null,
valueStoreProperty: "_state",
}
function hasManagedProperties(klass) {
return klass.hasOwnProperty(defaultPropertiesKey);
}
export function prepareClassForManagedPropertiesAfterCreation(klass) {
var {properties, propertySettings} = propertiesAndSettingsInHierarchyOf(klass);
klass[propertiesAndSettingsCacheSym] = {properties, propertySettings, classHierarchy: getClassHierarchy(klass)};
if (!properties || typeof properties !== "object") {
console.warn(`Class ${klass.name} indicates it has managed properties but its `
+ `properties accessor (${defaultPropertiesKey}) does not return `
+ `a valid property descriptor map`);
return;
}
prepareClassForProperties(klass, propertySettings, properties);
}
function prepareClassForProperties(klass, propertySettings, properties) {
ensurePropertyInitializer(klass);
var {
valueStoreProperty,
defaultGetter,
defaultSetter
} = propertySettings,
myProto = klass.prototype,
keys = Object.keys(properties);
keys.forEach(key => {
var descriptor = properties[key];
// ... define a getter to the property for the outside world...
var hasGetter = myProto.hasOwnProperty(key) && myProto.__lookupGetter__(key);
if (!hasGetter || hasGetter._wasGenerated) {
var getter = descriptor.get
|| (typeof defaultGetter === "function" && function() { return defaultGetter.call(this, key); })
|| function() { return this[valueStoreProperty][key]; };
getter._wasGenerated = true;
myProto.__defineGetter__(key, getter);
}
// ...define a setter if necessary
var hasSetter = myProto.hasOwnProperty(key) && myProto.__lookupSetter__(key);
if (!hasSetter || hasSetter._wasGenerated) {
var descrHasSetter = descriptor.hasOwnProperty("set"),
setterNeeded = descrHasSetter || !descriptor.readOnly;
if (setterNeeded) {
var setter = descriptor.set
|| (typeof defaultSetter === "function" && function(val) { defaultSetter.call(this, key, val); })
|| function(val) { this[valueStoreProperty][key] = val; };
setter._wasGenerated = true;
myProto.__defineSetter__(key, setter);
}
}
});
}
function ensurePropertyInitializer(klass) {
// when we inherit from "conventional classes" those don't have an
// initializer method. We install a stub that calls the superclass function
// itself
Object.defineProperty(klass.prototype, "propertiesAndPropertySettings", {
enumerable: false,
configurable: true,
writable: true,
value: function() {
var klass = this.constructor,
cached = klass[propertiesAndSettingsCacheSym];
if (cached) {
if (cached.classHierarchy != getClassHierarchy(klass)) {
let {properties, propertySettings} = propertiesAndSettingsInHierarchyOf(klass);
klass[propertiesAndSettingsCacheSym] = {
properties, propertySettings,
classHierarchy: getClassHierarchy(klass)
};
prepareClassForProperties(klass, propertySettings, properties);
} else {
return cached;
}
}
return klass[propertiesAndSettingsCacheSym] || propertiesAndSettingsInHierarchyOf(klass);
}
});
Object.defineProperty(klass.prototype, "initializeProperties", {
enumerable: false,
configurable: true,
writable: true,
value: function(values) {
var {properties, propertySettings} = this.propertiesAndPropertySettings();
prepareInstanceForProperties(this, propertySettings, properties, values)
return this;
}
});
}
function propertiesAndSettingsInHierarchyOf(klass) {
// walks class proto chain
var propertySettings = {...defaultPropertySettings},
properties = {},
allPropSettings = obj.valuesInPropertyHierarchy(klass, "propertySettings"),
allProps = obj.valuesInPropertyHierarchy(klass, "properties");
for (var i = 0; i < allPropSettings.length; i++) {
let current = allPropSettings[i];
current && typeof current === "object" && Object.assign(propertySettings, current);
}
for (var i = 0; i < allProps.length; i++) {
let current = allProps[i];
if (typeof current !== "object") {
console.error(
`[initializeProperties] ${klass} encountered property declaration ` +
`that is not a JS object: ${current}`);
continue;
}
// "deep" merge
for (var name in current) {
if (!properties.hasOwnProperty(name)) properties[name] = current[name];
else Object.assign(properties[name], current[name]);
}
}
return {properties, propertySettings};
}
function prepareInstanceForProperties(instance, propertySettings, properties, values) {
var {valueStoreProperty} = propertySettings,
sortedKeys = obj.sortKeysWithBeforeAndAfterConstraints(properties),
propsNeedingInitialize = [],
initActions = {};
// 1. this[valueStoreProperty] is were the actual values will be stored
if (!instance.hasOwnProperty(valueStoreProperty))
instance[valueStoreProperty] = {};
for (var i = 0; i < sortedKeys.length; i++) {
let key = sortedKeys[i],
descriptor = properties[key];
let derived = descriptor.derived, foldable = !!descriptor.foldable,
defaultValue = descriptor.hasOwnProperty("defaultValue") ?
descriptor.defaultValue : undefined;
if (Array.isArray(defaultValue)) defaultValue = defaultValue.slice();
if (!derived && !foldable) instance[valueStoreProperty][key] = defaultValue;
let initAction;
if (descriptor.hasOwnProperty("initialize")) {
initAction = initActions[key] = {initialize: defaultValue}
propsNeedingInitialize.push(key);
} else if (derived && defaultValue !== undefined) {
initAction = initActions[key] = {derived: defaultValue}
propsNeedingInitialize.push(key);
} else if (foldable && defaultValue !== undefined) {
initAction = initActions[key] = {folded: defaultValue}
propsNeedingInitialize.push(key);
}
if (values && key in values) {
if (descriptor.readOnly) {
console.warn(
`Trying to initialize read-only property ${key} in ${instance}, ` +
`skipping setting value`
);
} else {
if (!initAction) {
initAction = initActions[key] = {};
propsNeedingInitialize.push(key);
}
initAction.value = values[key];
}
}
}
// 2. Run init code for properties
// and if we have values we will initialize the properties from it. Values
// is expected to be a JS object mapping property names to property values
for (var i = 0; i < propsNeedingInitialize.length; i++) {
let key = propsNeedingInitialize[i],
actions = initActions[key],
hasValue = actions.hasOwnProperty("value");
// if we have an initialize function we call it either with the value from
// values or with the defaultValue
if (actions.hasOwnProperty("initialize")) {
let value = hasValue ? actions.value : actions.initialize;
properties[key].initialize.call(instance, value);
if (hasValue) instance[key] = actions.value;
}
// if we have a derived property we will call the setter with the default
// value or the value from values
else if (actions.hasOwnProperty("derived")) {
instance[key] = hasValue ? actions.value : actions.derived;
}
else if (actions.hasOwnProperty("folded")) {
instance[key] = hasValue ? actions.value : actions.folded;
}
// if we only have the value from values we simply call the setter with it
else if (hasValue) {
instance[key] = actions.value;
}
}
}