// @define Ext.Factory
/**
* @class Ext.Factory
* Manages factories for families of classes (classes with a common `alias` prefix). The
* factory for a class family is a function stored as a `static` on `Ext.Factory`. These
* are created either by directly calling `Ext.Factory.define` or by using the
* `Ext.mixin.Factoryable` interface.
*
* To illustrate, consider the layout system's use of aliases. The `hbox` layout maps to
* the `"layout.hbox"` alias that one typically provides via the `layout` config on a
* Container.
*
* Under the covers this maps to a call like this:
*
* Ext.Factory.layout('hbox');
*
* Or possibly:
*
* Ext.Factory.layout({
* type: 'hbox'
* });
*
* The value of the `layout` config is passed to the `Ext.Factory.layout` function. The
* exact signature of a factory method matches `{@link Ext.Factory#method!create}`.
*
* To define this factory directly, one could call `Ext.Factory.define` like so:
*
* Ext.Factory.define('layout', 'auto'); // "layout.auto" is the default type
*
* @since 5.0.0
*/
Ext.Factory = function(type) {
var me = this;
me.aliasPrefix = type + '.';
me.cache = {};
me.name = type.replace(me.fixNameRe, me.fixNameFn);
me.type = type;
/**
* @cfg {String} [creator]
* The name of the method used to prepare config objects for creation. This defaults
* to `'create'` plus the capitalized name (e.g., `'createLayout'` for the 'laoyut'
* alias family).
*/
me.creator = 'create' + Ext.String.capitalize(me.name);
};
Ext.Factory.prototype = {
/**
* @cfg {String} [aliasPrefix]
* The prefix to apply to `type` values to form a complete alias. This defaults to the
* proper value in most all cases and should not need to be specified.
*
* @since 5.0.0
*/
/**
* @cfg {String} [defaultProperty="type"]
* The config property to set when the factory is given a config that is a string.
*
* @since 5.0.0
*/
defaultProperty: 'type',
/**
* @cfg {String} [defaultType=null]
* An optional type to use if none is given to the factory at invocation. This is a
* suffix added to the `aliasPrefix`. For example, if `aliasPrefix="layout."` and
* `defaultType="hbox"` the default alias is `"layout.hbox"`. This is an alternative
* to `xclass` so only one should be provided.
*
* @since 5.0.0
*/
/**
* @cfg {String} [instanceProp="isInstance"]
* The property that identifies an object as instance vs a config.
*
* @since 5.0.0
*/
instanceProp: 'isInstance',
/**
* @cfg {String} [xclass=null]
* The full classname of the type of instance to create when none is provided to the
* factory. This is an alternative to `defaultType` so only one should be specified.
*
* @since 5.0.0
*/
/**
* @property {Ext.Class} [defaultClass=null]
* The Class reference of the type of instance to create when none is provided to the
* factory. This property is set from `xclass` when the factory instance is created.
* @private
* @readonly
*
* @since 5.0.0
*/
/**
* @cfg {String} [typeProperty="type"]
* The property from which to read the type alias suffix.
* @since 6.5.0
*/
typeProperty: 'type',
/**
* Creates an instance of this class family given configuration options.
*
* @param {Object/String} [config] The configuration or instance (if an Object) or
* just the type (if a String) describing the instance to create.
* @param {String} [config.xclass] The full class name of the class to create.
* @param {String} [config.type] The type string to add to the alias prefix for this
* factory.
* @param {String/Object} [defaultType] The type to create if no type is contained in the
* `config`, or an object containing a default set of configs.
* @return {Object} The newly created instance.
*
* @since 5.0.0
*/
create: function(config, defaultType) {
var me = this,
Manager = Ext.ClassManager,
cache = me.cache,
typeProperty = me.typeProperty,
alias, className, klass, suffix;
if (config) {
if (config[me.instanceProp]) {
return config;
}
if (typeof config === 'string') {
suffix = config;
config = {};
config[me.defaultProperty] = suffix;
}
className = config.xclass;
suffix = config[typeProperty];
}
if (defaultType && defaultType.constructor === Object) {
config = Ext.apply({}, config, defaultType);
defaultType = defaultType[typeProperty];
}
if (className) {
if (!(klass = Manager.get(className))) {
return Manager.instantiate(className, config);
}
}
else {
if (!(suffix = suffix || defaultType || me.defaultType)) {
klass = me.defaultClass;
}
//<debug>
if (!suffix && !klass) {
Ext.raise('No type specified for ' + me.type + '.create');
}
//</debug>
if (!klass && !(klass = cache[suffix])) {
alias = me.aliasPrefix + suffix;
className = Manager.getNameByAlias(alias);
// this is needed to support demand loading of the class
if (!(klass = className && Manager.get(className))) {
return Manager.instantiateByAlias(alias, config);
}
cache[suffix] = klass;
}
}
return klass.isInstance ? klass : new klass(config);
},
fixNameRe: /\.[a-z]/ig,
fixNameFn: function(match) {
return match.substring(1).toUpperCase();
},
clearCache: function() {
this.cache = {};
this.instanceCache = {};
},
/**
* Sets a hook on the creation process. If the hook `fn` returns `undefined` then
* the original `create` method is called.
*
* @param {Function} fn The hook function to call when `create` is invoked.
* @param {Function} fn.original The original `create` method.
* @param {String/Object} fn.config See {@link #method!create create}.
* @param {String/Object} fn.defaultType See {@link #method!create create}.
* @private
* @since 6.5.0
*/
hook: function(fn) {
var me = this,
original = me.create;
me.create = function(config, defaultType) {
var ret = fn.call(me, original, config, defaultType);
if (ret === undefined) {
ret = original.call(me, config, defaultType);
}
return ret;
};
},
/**
* This method accepts a `config` object and an existing `instance` if one exists
* (can be `null`).
*
* The details are best explained by example:
*
* config: {
* header: {
* xtype: 'itemheader'
* }
* },
*
* applyHeader: function (header, oldHeader) {
* return Ext.Factory.widget.update(oldHeader, header,
* this, 'createHeader');
* },
*
* createHeader: function (header) {
* return Ext.apply({
* xtype: 'itemheader',
* ownerCmp: this
* }, header);
* }
*
* Normally the `applyHeader` method would have to coordinate potential reuse of
* the `oldHeader` and perhaps call `setConfig` on it with the new `header` config
* options. If there was no `oldHeader`, of course, a new instance must be created
* instead. These details are handled by this method. If the `oldHeader` is not
* reused, it will be {@link Ext.Base#method!destroy destroyed}.
*
* For derived class flexibility, the pattern of calling out to a "creator" method
* that only returns the config object has become widely used in many components.
* This pattern is also covered in this method. The goal is to allow the derived
* class to `callParent` and yet not end up with an instantiated component (since
* the type may not yet be known).
*
* This mechanism should be used in favor of `Ext.factory()`.
*
* @param {Ext.Base} instance
* @param {Object/String} config The configuration (see {@link #method!create}).
* @param {Object} [creator] If passed, this object must provide the `creator`
* method or the `creatorMethod` parameter.
* @param {String} [creatorMethod] The name of a creation wrapper method on the
* given `creator` instance that "upgrades" the raw `config` object into a final
* form for creation.
* @param {String} [defaultsConfig] The name of a config property (on the provided
* `creator` instance) that contains defaults to be used to create instances. These
* defaults are present in the config object passed to the `creatorMethod`.
* @return {Object} The reconfigured `instance` or a newly created one.
* @since 6.5.0
*/
update: function(instance, config, creator, creatorMethod, defaultsConfig) {
var me = this,
aliases, defaults, reuse, type;
// If config is falsy or a valid instance, destroy the current instance
// (if it exists) and replace with the new one
if (!config || config.isInstance) {
//<debug>
if (config && !config[me.instanceProp]) {
Ext.raise('Config instance failed ' + me.instanceProp + ' requirement');
}
//</debug>
if (instance && instance !== config) {
instance.destroy();
}
return config;
}
if (typeof config === 'string') {
type = config;
config = {};
config[me.defaultProperty] = type;
}
// See if the existing instance can just be reconfigured:
if (instance) {
if (config === true) {
return instance;
}
if (!(type = config.xclass)) {
if (!(type = config.xtype)) {
type = config[me.typeProperty];
if (type) {
// instance must have the right alias...
type = me.aliasPrefix + type;
aliases = instance.self.prototype;
// The alias for the class is on the prototype (derived
// classes do not really own their inherited aliases since
// they won't be created when using them):
if (aliases.hasOwnProperty('alias')) {
aliases = aliases.alias;
if (aliases) {
reuse = aliases === type || aliases.indexOf(type) > -1;
}
}
}
}
else {
// config = { xtype: ... }
reuse = instance.isXType(type, /* shallow= */ true);
}
}
else {
// config = { xclass: ... } so we're good if they match
reuse = instance.$className === type;
}
if (reuse) {
instance.setConfig(config);
return instance;
}
instance.destroy();
}
if (config === true) {
config = {};
}
if (creator) {
if (defaultsConfig) {
defaults = Ext.Config.map[defaultsConfig];
defaults = creator[defaults.names.get]();
if (defaults) {
config = Ext.merge(Ext.clone(defaults), config);
}
}
creatorMethod = creatorMethod || me.creator;
if (creator[creatorMethod]) {
config = creator[creatorMethod](config);
//<debug>
if (!config) {
Ext.raise('Missing return value from ' + creatorMethod + ' on class ' +
creator.$className);
}
//</debug>
}
}
return me.create(config);
}
};
/**
* For example, the layout alias family could be defined like this:
*
* Ext.Factory.define('layout', {
* defaultType: 'auto'
* });
*
* To define multiple families at once:
*
* Ext.Factory.define({
* layout: {
* defaultType: 'auto'
* }
* });
*
* @param {String} type The alias family (e.g., "layout").
* @param {Object/String} [config] An object specifying the config for the `Ext.Factory`
* to be created. If a string is passed it is treated as the `defaultType`.
* @return {Function}
* @static
* @since 5.0.0
*/
Ext.Factory.define = function(type, config) {
var Factory = Ext.Factory,
cacheable = config && config.cacheable,
defaultClass, factory, fn;
if (type.constructor === Object) {
Ext.Object.each(type, Factory.define, Factory);
}
else {
factory = new Ext.Factory(type);
if (config) {
if (config.constructor === Object) {
Ext.apply(factory, config);
if (typeof(defaultClass = factory.xclass) === 'string') {
factory.defaultClass = Ext.ClassManager.get(defaultClass);
}
}
else {
factory.defaultType = config;
}
}
/*
* layout = Ext.Factory.layout('hbox');
*/
Factory[factory.name] = fn = function(config, defaultType) {
// maintain indirection through "create" name on instance to allow
// the hook() mechanism to replace it.
return factory.create(config, defaultType);
};
if (cacheable) {
factory.instanceCache = {};
factory.hook(function(original, config, defaultType) {
var cache = this.instanceCache,
v;
if (typeof config === 'string' && !(v = cache[config])) {
v = original.call(this, config, defaultType);
// Validator may have cacheable:false to force new instances each time,
// avoiding the cache
if (v.cacheable !== false) {
cache[config] = v;
//<debug>
// this should catch some improper modifications to the shared
// cached instance, during development but not in production.
Ext.Object.freeze(v);
//</debug>
}
}
return v;
});
}
fn.instance = factory;
/*
* Typically called by an applier:
*
* applyLayout: function (layout, oldLayout) {
* return Ext.Factory.layout.update(oldLayout, layout, this);
* },
*
* createLayout: function (config) {
* return Ext.apply({
* //.. stuff
* }, config);
* }
*/
fn.update = function(instance, config, creator, creatorMethod, defaultsConfig) {
return factory.update(instance, config, creator, creatorMethod, defaultsConfig);
};
}
return fn;
};
Ext.Factory.clearCaches = function() {
var Factory = Ext.Factory,
key, item;
for (key in Factory) {
item = Factory[key];
item = item.instance;
if (item) {
item.clearCache();
}
}
};
Ext.Factory.on = function(name, fn) {
Ext.Factory[name].instance.hook(fn);
};
/**
* This mixin automates use of `Ext.Factory`. When mixed in to a class, the `alias` of the
* class is retrieved and combined with an optional `factoryConfig` property on that class
* to produce the configuration to pass to `Ext.Factory`.
*
* The factory method created by `Ext.Factory` is also added as a static method to the
* target class.
*
* Given a class declared like so:
*
* Ext.define('App.bar.Thing', {
* mixins: [
* 'Ext.mixin.Factoryable'
* ],
*
* alias: 'bar.thing', // this is detected by Factoryable
*
* factoryConfig: {
* defaultType: 'thing', // this is the default deduced from the alias
* // other configs
* },
*
* ...
* });
*
* The produced factory function can be used to create instances using the following
* forms:
*
* var obj;
*
* obj = App.bar.Thing.create('thing'); // same as "new App.bar.Thing()"
*
* obj = App.bar.Thing.create({
* type: 'thing' // same as above
* });
*
* obj = App.bar.Thing.create({
* xclass: 'App.bar.Thing' // same as above
* });
*
* var obj2 = App.bar.Thing.create(obj);
* // obj === obj2 (passing an instance returns the instance)
*
* Alternatively the produced factory is available as a static method of `Ext.Factory`.
*
* @since 5.0.0
*/
Ext.define('Ext.mixin.Factoryable', {
mixinId: 'factoryable',
onClassMixedIn: function(targetClass) {
var proto = targetClass.prototype,
factoryConfig = proto.factoryConfig,
alias = proto.alias,
config = {},
dot, createFn;
alias = alias && alias.length && alias[0];
if (alias && (dot = alias.lastIndexOf('.')) > 0) {
config.type = alias.substring(0, dot);
config.defaultType = alias.substring(dot + 1);
}
if (factoryConfig) {
delete proto.factoryConfig;
Ext.apply(config, factoryConfig);
}
createFn = Ext.Factory.define(config.type, config);
if (targetClass.create === Ext.Base.create) {
// allow targetClass to override the create method
targetClass.create = createFn;
}
}
/**
* @property {Object} [factoryConfig]
* If this property is specified by the target class of this mixin its properties are
* used to configure the created `Ext.Factory`.
*/
});