The class system
All Bryntum classes extend a Base class which provides services which the subclasses need and which simplify object
creation and configuration.
New classes which participate in the Bryntum ecosystem should use this convention.
Construction
Subclasses which inherit from Base must not implement a constructor.
Instead, they may implement a construct method which will be called immediately upon construction, passed the same
parameters as the caller passed to the constructor.
This lifts the restriction that the this reference is not available until the super call. Subclasses may perform
certain operations upon themselves based upon incoming configuration information before
calling super.construct(config) and passing all the parameters upwards.
Note that due to automatic application of configuration by the Base class, it is often not necessary to write
a construct method at all. Processing of incoming configuration values should be done in changeConfigName
and updateConfigName functions which may be implemented for named "configurable" config properties.
The purpose of a constructor is only to initialize private instance properties which are not available for configuration, but which are needed for class operation.
Configuration
When this construct call chain arrives at the Base class, the construct method there applies the configuration.
Configurable properties are defined in a static class method:
static configurable = {
/**
* The name of the object
* @config {String}
*/
name : null,
/**
* The type of the object
* @config {String}
*/
type : null
};
For each of these, when set, the incoming value is passed through an ingestion process which may convert simple incoming values to a final form which will be stored as that property value.
Changers
If a configurable property needs to be "promoted" from a simple value into a final stored form, this is done by a changer function. This is named according to the configurable property's name:
changeName(name, oldName) {
return StringHelper.capitalize(name);
}
A system-generated setter will call the changer, passing in the new, incoming value, and the old value. The value which the property must be set to must be returned from the function.
If the function returns undefined, the property set operation is vetoed.
If the function returns a value that is equal to the old value, the property set operation is vetoed.
Note that the underlying property named _name is set by the system-generated setter as the result of the changer,
you do not have to set the property.
Updaters
If a new property value is set, in order to postprocess the new, incoming value, an updater function is called. This is named according to the property's name:
updateName(name, oldName) {
// The system ensures that type will be set as configured
this.setTitleText(`Client name ${name}, type : ${this.type}`);
}
You may have noticed that the updater of the name property requires that the type property already be ingested.
This issue is handled automagically by the configuration system as described in the next section.
Config property ingestion ordering
Configuration properties are applied in a very special way. As part of the Base class's configuration, the incoming
config object is iterated and each property is set.
However, some configurations may depend on the presence, and preprocessing by the changer or updater of other configurations.
For example the data property of the Grid class depends upon the incoming store config having been
preprocessed and converted into a Store instance and cached to be available.
This could be done by writing a constructor which reads the incoming config object in a very specific order, and applies the configs correctly.
But this approach is complex and brittle, and there are often interwoven and complex dependencies which may change as the class changes.
The approach taken by the Base class is that for each incoming config property a temporary instance property is
defined, and the getter of that property will delete that temporary property, read the value from the incoming
configuration object, then set that property which will then pass through the class author's changer and updater
to become the true property value.
Then that temporary property getter returns the value of the property which will come through the system-generated
getter and the required, fully promoted value will be returned.
In this way, the construct function may be almost empty. All initialization takes place in property changers and
updaters which can rely on the presence of whatever configuration properties they need.
During initial configuration, the isConfiguring property is set to true on the instance so that setters can tell
whether the property is being set at instantiation time or later.
And for each config being set an initializingPropName flag is set so that if the setter ends up calling elsewhere, it
can always be determined that it is because of a certain config's initialization.
Class properties versus configurations
Certain properties that a class may need to use should not be passed through this mechanism because there is a slight overhead (the benefits described above far outweigh this in terms of robustness and flexibility).
If for example there are constant values, for example a CSS class name to be applied to a DOM element, then either
place it in a module-private const:
import Base from "../Base.js";
const
myCssClassName = 'my-css-class',
myRegExp = /^\s*(\w+)\s*:(.*)$/i;
export default class MyClass extends Base {
}
or, if it needs to be visible to outside code, place it onto the prototype:
import Base from "../Base.js";
export default class MyClass extends Base {
}
MyClass.prototype.myCssClassName = 'my-css-class';
If the class needs to initialize per-instance properties for internal use then set them up in the construct method
before calling super.construct(config)
import Base from "../Base.js";
export default class MyClass extends Base {
construct(config) {
this.addedRecords = [];
this.removedRecords = [];
this.modifiedRecords = [];
super.construct(config);
}
}
Further reading
For detailed information on creating custom UI widgets (classes that extend Widget), including the compose() method,
DomConfig, lifecycle methods, and styling, see the Creating Widgets guide.