Extensibility

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.

Listing 1. SpecialStringField.ts
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:

Listing 2. index.ts
// ...
export * from './SpecialStringField';
// ...
ObjectFactory.get().registerNamespace('yournamespace', self);
Listing 3. ExampleFormModel.ts
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:

Listing 4. Adding a new object factory registration
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.

Listing 5. ExtendedForm.ts
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.

Listing 6. ExtendedForm.ts with separate 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.

Listing 7. ExtendedFormModel.ts
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.

Listing 8. CustomApp.ts
import {App, Extension} from '@eclipse-scout/core';
import {MyExtension} from './index';

export class CustomApp extends App {
  override _installExtensions() {
    Extension.install([
      MyExtension
    ]);
  }
}
Listing 9. Entry point file
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 class AbstractStringFieldExtension.

  • AbstractCodeType is extensible. Therefore, there is a class AbstractCodeTypeExtension.

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:

scout extensibility chain concept

Extending a StringField example

The following example changes the initial value of a StringField called NameField:

Listing 10. Extension for 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:

Listing 11. Register extension for NameField
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.

Listing 12. Extension for PersonForm
/**
 * 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:

Listing 13. Register extension for PersonForm
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!

Listing 14. Extension Data for PersonForm
@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:

Listing 15. Access extended fields
// 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:

Listing 16. Extension for PersonForm
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:

Listing 17. Move NameField to LastBox
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().