Extensibility
This document is referring to a past Scout release. Please click here for the recent version. |
Scout JS Extensibility
The extensibility concepts of Scout allow you to extend or even replace methods of Scout widgets or objects. You can also use it to extend your own objects or objects from a Scout based third party library.
There are mainly two ways to extend an object:
-
Extension by Sub-Classing
-
Extension by Composition
Extension by Sub-Classing
Extending an object using sub-classing is simple and straight forward. It can either be used to create a custom widget and only use it for certain cases. And it can even be used to replace a specific widget completely, so your widget will be used every time the original widget is requested.
To extend from a widget, just create a new class, extend from the desired widget class and override the methods you want to adjust.
import {StringField} from '@eclipse-scout/core';
export class SpecialStringField extends StringField {
// Override desired methods
}
Then, register it in your index
file as usual and use it in your code by creating a new instance with scout.create(SpecialStringField)
or as part of a model:
// ...
export * from './SpecialStringField';
// ...
ObjectFactory.get().registerNamespace('yournamespace', self);
import {FormModel, GroupBox} from '@eclipse-scout/core';
import {SpecialStringField} from './index';
export default (): FormModel => ({
rootGroupBox: {
objectType: GroupBox,
fields: [
{
id: 'SpecialField',
objectType: SpecialStringField,
label: 'Your special field'
}
]
}
});
/* **************************************************************************
* GENERATED WIDGET MAPS
* **************************************************************************/
export type ExampleFormWidgetMap = {
'SpecialField': SpecialStringField;
};
If you want to replace every StringField
in your application with SpecialStringField
, you need to register a new object factory for the objectType
StringField
as follows:
import {scout} from '@eclipse-scout/core';
import {SpecialStringField} from './index';
scout.addObjectFactories({
'StringField': () => new SpecialStringField()
});
More details can be found in the chapter Object Factory.
Extending a Model
If you need to extend a widget that uses a model (see Creating a Widget Declaratively), you may have to extend that model as well.
To do so, extend the widget as described above and either adjust the widgets in the init
function directly.
import {InitModelOf} from '@eclipse-scout/core';
import {ExampleForm} from './ExampleForm';
export class ExtendedForm extends ExampleForm {
protected override _init(model: InitModelOf<this>) {
super._init(model);
this.widget('SpecialField').setLabel('New label for the special field');
}
}
Or, for more complex cases, you can put your model adjustments in a separate file and use the declarative approach.
To do so, override the _jsonModel
method and use models.extend
to adapt the original model.
import {FormModel, models} from '@eclipse-scout/core';
import {ExampleForm} from './index';
import ExtendedFormModel from './ExtendedFormModel';
export class ExtendedForm extends ExampleForm {
protected override _jsonModel(): FormModel {
return models.extend(ExtendedFormModel, super._jsonModel());
}
}
Then, create a new file that will contain your model extensions.
-
Use the
target
keyword to specify which widget should be adjusted. -
With the
operation
keyword you define, whether properties should be adjusted (appendTo
) or new objects should be inserted (insert
).
The following example contains an extension for the field with the id SpecialField
that sets a new value for the label.
import {ExtensionModel} from '@eclipse-scout/core';
export default (): ExtensionModel => ({
type: 'extension',
extensions: [
{
operation: 'appendTo',
target: {
id: 'SpecialField'
},
extension: {
label: 'New label for the special field'
}
}
]
});
Extension by Composition
Extension by Composition allows to have multiple, independent extensions of a Scout object.
It also allows the adjustment of super classes of objects from which it is not possible to inherit, e.g. FormField
or even Widget
.
This extension feature works by wrapping functions on the prototype of a Scout object with a wrapper function which is provided by an extension. The extension feature doesn’t rely on subclassing, instead we simply register one or more extensions for a single Scout class. When a function is called on an extended object, the functions are called on the registered extensions first. Since a Scout class can have multiple extensions, we speak of an extension chain, where the last element of the chain is the original (extended) object.
The base class for all extensions is Extension
. This class is used to extend an existing
Scout object. In order to use the extension feature you must subclass Extension and
implement an init
function, where you register the functions you want to extend. Example:
import {Extension, StringField} from '@eclipse-scout/core';
export class MyExtension extends Extension {
init() {
this.extend(StringField.prototype, '_init');
}
}
Then you implement functions with the same name and signature on the extension class. Example:
import {Extension, InitModelOf, StringField} from '@eclipse-scout/core';
export class MyExtension extends Extension<StringField> {
init() {
this.extend(StringField.prototype, '_init');
}
_init(model: InitModelOf<StringField>) {
// Call the original _init() method of the StringField class
this.next(model);
// Extend the instance with a new property called bar with the value foo
// -> EVERY string field now has this new property
this.extended.setProperty('bar', 'foo');
}
}
The extension feature sets two properties on the extension instance before the extended method is called. These two properties are described below. The function scope (this) is set to the extension instance when the extended function is called.
- next
-
is a reference to the next extended function or the original function of the extended object, in case the current extension is the last extension in the extension chain.
- extended
-
is the extended or original object.
All extensions must be registered in the _installExtensions
function of your App
.
You can find your app in your entrypoint file that is linked in your webpack.config.js
.
If you already have a custom App
, just override _installExtensions
and register the extension.
Otherwise, you need to create a custom App
first by extending from the Scout App
(or RemoteApp
for Scout Classic) and make sure
this new app is initialized rather than the default one.
import {App, Extension} from '@eclipse-scout/core';
import {MyExtension} from './index';
export class CustomApp extends App {
override _installExtensions() {
Extension.install([
MyExtension
]);
}
}
import CustomApp from './CustomApp';
let app = new CustomApp();
app.init();
Scout Classic Extensibility
Required version: The API described here requires Scout version 4.2 or newer. |
Overview
When working with large business applications it is often required to split the application into several modules. Some of those modules may be very basic and can be reused in multiple applications. For those it makes sense to provide them as binary library. But what if you have created great templates for your applications but in one special case you want to include one more column in a table or want to execute some other code when a pre-defined context menu is pressed? You cannot just modify the code because it is a general library used everywhere. This is where the extensibility concept helps.
To achieve this two new elements have been introduced:
-
Extension Classes: Contains modifications for a target class. Modifications can be new elements or changed behavior of existing elements.
-
Extension Registry: Service holding all Extensions that should be active in the application.
The Scout extensibility concept offers three basic possibilities to extend existing components:
-
Extensions Changing behavior of a class
-
Contributions Add new elements to a class
-
Moves Move existing elements within a class
The following chapters will introduce this concepts and present some examples.
Extensions
Extensions contain modifications to a target class. This target class must be extensible. All elements that implement org.eclipse.scout.rt.shared.extension.IExtensibleObject
are extensible. And for all extensible elements there exists a corresponding abstract extension class.
Examples:
-
AbstractStringField
is extensible. Therefore, there is a classAbstractStringFieldExtension
. -
AbstractCodeType
is extensible. Therefore, there is a classAbstractCodeTypeExtension
.
Target classes can be all that are instanceof those extensible elements. This means an AbstractStringFieldExtension
can be applied to AbstractStringField
and all child classes.
Extensions contain methods for all Scout operations (inherited methods starting with exec
). Those methods have the same signature except that they have one more input parameter. This method allows you to intercept the given Scout Operation and execute your own code even though the declaring class exists in a binary library. It is then your decision if you call the original code or completely replace it. To achieve this the Chain Pattern is used: All extensions for a target class are called as part of a chain. The order is given by the order in which the extensions are registered. And the original method of the Scout element is an extension as well.
Extensions to specific types of elements are prepared as abstract classes:
-
AbstractGroupBoxExtension
-
AbstractImageFieldExtension
The following image visualizes the extension chain used to intercept the default behavior of a component:
Extending a StringField example
The following example changes the initial value of a StringField called NameField
:
public class NameFieldExtension extends AbstractStringFieldExtension<NameField> {
public NameFieldExtension(NameField owner) {
super(owner);
}
@Override
public void execInitField(FormFieldInitFieldChain chain) {
chain.execInitField(); // call the original exec init. whatever it may do.
getOwner().setValue("FirstName LastName"); // overwrite the initial value of the name field
}
}
Note: The type parameter of the extension (e.g. NameField
) denotes the element which is extended.
The extension needs to be registered when starting the application:
Jobs.schedule(() -> BEANS.get(IExtensionRegistry.class).register(NameFieldExtension.class), Jobs.newInput()
.withRunContext(ClientRunContexts.copyCurrent())
.withName("register extension"));
Contributions
The section before explained how to modify the behavior of existing Scout elements. This section will describe how to contribute new elements into existing containers.
This is done by using the same mechanism as before. It is required to create an Extension too. But instead of overwriting any Scout Operation we directly define the new elements within the Extension. A lot of new elements can be added this way: Fields, Menus, Columns, Codes, …
Some new elements may also require a new DTO (FormData, TablePageData, TableData) to be filled with data from the server. The corresponding DTO for the extension is automatically created when using the Scout Plugin 4.2 or newer in your IDE and having the @Data
annotation specified on your extension. As soon as the DTO extension has been registered in the IExtensionRegistry
service it is automatically created when the target DTO is created and will also be imported and exported automatically!
The following example adds two new fields for salary and birthday to a PersonForm
. Please note the @Data
annotation which describes where the DTO for this extension should be created.
/**
* Extension for the MainBox of the PersonForm
*/
@Data(PersonFormMainBoxExtensionData.class)
public class PersonFormMainBoxExtension extends AbstractGroupBoxExtension<MainBox> {
public PersonFormMainBoxExtension(MainBox ownerBox) {
super(ownerBox);
}
@Order(2000)
@ClassId("fda7cd67-0df1-4194-9d70-22a9b3ce890d")
public class SalaryField extends AbstractBigDecimalField {
}
@Order(3000)
@ClassId("478037fb-759f-4fa1-b737-c77f903c6881")
public class BirthdayField extends AbstractDateField {
}
}
Beware: Field names must be unique throughout form and extensions (e.g. there may not be a field on the form or another extension contributing to the same form with the same field name). However, it is possible to create templates (e.g. a group box as container with its own @FormData annotation) which is added multiple times through a form or extensions.
The extension data must be registered manually in the job like in the example before:
BEANS.get(IExtensionRegistry.class).register(PersonFormMainBoxExtension.class);
Then the Scout IDE plugin automatically creates the extension DTO which could look as follows. Please note: The DTO is generated automatically, but you have to register the generated DTO manually!
@Extends(PersonFormData.class)
@Generated(value = "org.eclipse.scout.docs.snippets.person.PersonFormMainBoxExtension", comments = "This class is auto generated by the Scout SDK. No manual modifications recommended.")
public class PersonFormMainBoxExtensionData extends AbstractFormFieldData {
private static final long serialVersionUID = 1L;
public Birthday getBirthday() {
return getFieldByClass(Birthday.class);
}
public Salary getSalary() {
return getFieldByClass(Salary.class);
}
@ClassId("478037fb-759f-4fa1-b737-c77f903c6881-formdata")
public static class Birthday extends AbstractValueFieldData<Date> {
private static final long serialVersionUID = 1L;
}
@ClassId("fda7cd67-0df1-4194-9d70-22a9b3ce890d-formdata")
public static class Salary extends AbstractValueFieldData<BigDecimal> {
private static final long serialVersionUID = 1L;
}
}
You can also access the values of the DTO extension as follows:
// create a normal FormData
// contributions are added/imported/exported automatically
PersonFormData data = new PersonFormData();
// access the data of an extension
PersonFormMainBoxExtensionData c = data.getContribution(PersonFormMainBoxExtensionData.class);
c.getSalary().setValue(new BigDecimal("200.0"));
Extending a form and a handler
Extending a AbstractForm and one (or more) of its AbstractFormHandlers that can be achieved as follows:
public class PersonFormExtension extends AbstractFormExtension<PersonForm> {
public PersonFormExtension(PersonForm ownerForm) {
super(ownerForm);
}
@Override
public void execInitForm(FormInitFormChain chain) {
chain.execInitForm();
// Example logic: Access the form, disable field
getOwner().getNameField().setEnabled(false, true, true);
}
public void testMethod() {
MessageBoxes.create().withHeader("Extension method test").withBody("A method from the form extension was called").show();
}
public static class NewFormHandlerExtension extends AbstractFormHandlerExtension<NewHandler> {
public NewFormHandlerExtension(NewHandler owner) {
super(owner);
}
@Override
public void execPostLoad(FormHandlerPostLoadChain chain) {
chain.execPostLoad();
// Example logic: Show a message box after load
MessageBoxes.create().withHeader("Extension test").withBody("If you can read this, the extension works correctly").show();
// Access element from the outer extension.
PersonFormExtension extension = ((AbstractForm) getOwner().getForm()).getExtension(PersonFormExtension.class);
extension.testMethod();
}
}
}
There are a few things to note about this example:
-
It is only necessary to register the outer form extension, not the inner handler extension as well.
-
The inner handler extension must be
static
, otherwise an Exception will occur when the extended form is being started! -
You can access the element you are extending by calling
getOwner()
. -
Since you cannot access elements from your form extension directly from the inner handler extension (because it is static), you will need to retrieve the form extension via the
getExtension(Class<T extends IExtension<?>>)
method on the extended object, as done here to retrieve the form extension from the form handler extension.
Move elements
You can also move existing Scout elements to other positions. For this you have to register a move command in the IExtensionRegistry. As with all extension registration it is added to the extension registration Job in your Activator class:
BEANS.get(IExtensionRegistry.class).registerMove(NameField.class, 20d, LastBox.class);
Migration
The new extensibility concept is added on top of all existing extension possibilities like injection or sub-classing. Therefore, it works together with the current mechanisms. But for some use cases (like modifying template classes) it offers a lot of benefits. Therefore, no migration is necessary. The concepts do exist alongside each others.
However, there is one impact: Because the Scout Operation methods are now part of a call chain they may no longer be invoked directly. So any call to e.g. execValidateValue()
is no longer allowed because this would exclude the extensions for this call. The Scout SDK marks such calls with error markers in the Eclipse Problems view. If really required the corresponding intercept-Method can be used. So instead directly calling myField.execChangedValue
you may call myField.interceptChangedValue()
.