Inheriting advanced widgets

Hi,
in this tutorial, I will show you how to inherit a more complex widget. For this tutorial I asume that you have read basic widget tutorial before.

In this example, I want to extend the DropDown widget. The DropDown widget does exactly what the name says, it creates a drop-down list of items. If the widget is near the bottom of your screen, this creates the problem that the drop-down list leaves the screen at the bottom and you can no longer select an item from that list. I will extend the DropDown widget with the option to drop up a list or center a list to screen middle so that it always uses the maximum space available.

As a starting point, we follow the exact same steps as we did in the basic tutorial. This gives us an exact copy of the widget with all its functionality. The function that we would like to modify is DropDownBoxView and is stored in original widget folder under “/lib/view/DropDownBoxView.js”. This funtion implements the list box that pops out when you click the widget. Remember that we can only derive complete widgets but we can not reference any files that are inside a brease widget folder.

We need to copy the file DropDownBoxView.js and all its dependencies into our widget folder. In total we need to copy these 5 files from the original widget into our own widget folder.

The files that we just copied use dependencies that do no longer work when the files are not in their original location. The references point into the widget brease framework that we can not reach from our widget library. This means that we need to modify these references. Our first goal is to fix these references and make the code work as it is.

We start with the file DropDownBoxView.js The start of the file looks like this

'use strict';
define([
    'widgets/brease/common/libs/redux/view/ListView/ListView',
    'widgets/brease/common/libs/redux/view/ItemView/ItemView',
    'widgets/brease/DropDownBox/libs/reducer/DropDownBoxActions',
    'brease'
], function (ListView, ItemView, DropDownBoxActions, { appView, bodyEl, enum: { Enum }, core: { Utils }, events: { BreaseEvent, EventDispatcher }, controller: { popUpManager } }) {
    /**
    * @class widgets.brease.DropDownBox.libs.view.DropDownBoxView 
    */

The function references several items from “widgets/brease/common/libs”. Unfortunately, the API does not allow access to all components in this path. You will see later on that there are more references to the common path. Instead of copying one file at a time, we take the complete “libs” folder and make a copy in our widget library in our own “common” folder. This has the advantage that we can use this common folder in the future without picking every single file again.

:information_source: Keep in mind that this common folder could change in the future with another mappView version at which point you have to repeat the process again.

The result should look like this:


…

:high_voltage: What we do next is something that I have tried with the drop-down box, and it worked fine, but I can not guarantee that this does not have any side effects that I do not know about.

The common folder that we just copied has tons of references to the brease framework that we need to modify. Right click on your “common/libs” folder and select “Find in folder”

Enter the following text and hit the replace all button

Repeat the same with this text and hit the replace all button

With a little bit of luck, everything in libs should now point to our common library. We can now go ahead and modify the references in the DropDownBoxView.js file from “brease” to brXtended" to point to our library.

'use strict';
define([
    'widgets/brXtended/common/libs/redux/view/ListView/ListView',
    'widgets/brXtended/common/libs/redux/view/ItemView/ItemView',
    'widgets/brXtended/DropDownBox/libs/reducer/DropDownBoxActions',
    'brease'
], function (ListView, ItemView, DropDownBoxActions, { appView, bodyEl, enum: { Enum }, core: { Utils }, events: { BreaseEvent, EventDispatcher }, controller: { popUpManager } }) {

We repeat the same process with all other files in our widget libs folder.


Wherever something points to “widgets/brease/common/libs” we change it to “widgets/brXtended/common/libs”

:information_source: This is a good time to do a final test before we modify something to make sure that the widget still works as expected. After that we can modify the code.

I will not explain every line that I have modified, just the parts that are important to modify an derived widget function. The first file we change is the Config.js We want to introduce a new parameter called “listPositionExt”. This new parameter has the following options:

  • If 0, list position extension is off.
  • If 1, the list box will reverse the pop-up direction. For ex. listPosition=‘right’ from bottom upwards.
  • If 2, the list box will always try to center the list box to the screen.

Since we derive from an existing widget, including the configuration, we can pretty much delete everything from the original config file except the return value. We then add our own parameter and the file should look like this. Don’t forget to include the new parameter in the return structure!

'use strict';
define(['brease'], function ({ enum: { Enum } }) {

    /**
     * @class widgets.brXtended.DropDownBox.config.Config
     * @extends core.javascript.Object
     * @override widgets.brXtended.DropDownBox
     */

     /**
     * @cfg {Integer} listPositionExt=0
     * @iatStudioExposed
     * @iatCategory Extended
     * If 0, list position extension is off.
     * If 1, the list box will reverse the pop-up direction. For ex. listPosition='right' from bottom upwards.
     * If 2, the list box will always try to center the list box to the screen.
     */

    return {
        selectedIndex: 0,
        selectedValue: '',
        itemHeight: 40,
        imageAlign: Enum.ImageAlign.left,
        imagePath: '',
        ellipsis: false,
        wordWrap: false,
        multiLine: false,
        fitHeight2Items: true,
        dataProvider: [],
        listPosition: Enum.Position.right,
        listPositionExt: 0,
        listWidth: 150,
        maxVisibleEntries: 4,
        cropToParent: Enum.CropToParent.none,
        displaySettings: Enum.DropDownDisplaySettings.default,
        tabIndex: 0
    };
});

The next file we modify is the main file DropDownBox.js Here we have to add all references that we will use later to replace the DropDownBoxView. This is something that you might do at the end when you know what references you really used in this file.

'use strict';
define([
    'brease',
    'widgets',
    './libs/config/Config',
    './libs/view/DropDownBoxView/DropDownBoxView',
    './libs/config/InitState',
    './libs/reducer/DropDownBoxReducer',
    'widgets/brXtended/common/libs/external/redux',
], function (
    brease,
    widgets,
    Config,
    DropDownBoxView,
    InitState,
    DropDownBoxReducer,
    Redux,
)
{

We then add our standard code with the reference to the brease widget and point to our modified config file.

{

    /**
     * @class widgets.brXtended.DropDownBox
     *
     * @mixins widgets.brease.common.DragDropProperties.libs.DroppablePropertiesEvents
     *
     * DropDownBox
     * @extends widgets.brease.DropDownBox
     *
     * @iatMeta studio:visible
     * true
     * @iatMeta category:Category
     * Selector
     * @iatMeta description:short
     * DropDownliste von Texten
     * @iatMeta description:de
     * Zeigt eine DropDownliste, aus welcher der Benutzer Elemente auswählen kann
     * @iatMeta description:en
     * Displays a drop-down list where the user can select items
     */

    var defaultSettings = Config

The next code block is pretty standard, so I skip over that.

We have to modify the init function so that the widget uses our DropDownBoxView and not the one that comes with the derived widget. To be honest, this was a lot of trial and error with the original init function, so I am not going to pretend that I fully understand everything that is going on here. I used the original init function and found the line where the DropDownBoxView is created.

this.dropDownBoxView = new DropDownBoxView(this.store, this.el, this);

I worked my way back and left everything that is needed to create the DropDownBoxView which is basically this:

         // Calculate init state
        var initState = InitState.calculateInitState(this.settings, this.isEnabled(), this.isVisible());

        // Add additional property to init state
        initState.items.listSettings.listPositionExt = this.settings.listPositionExt;

        // Create store
        this.store = Redux.createStore(DropDownBoxReducer, initState);

The next part is important because here we remove the original DropDownBoxView from the derived widget

        // Remove inherited view
        window.removeEventListener('addEventListener', this.focusHandler.onRender.bind(this.focusHandler));
        this.dropDownBoxView.dispose();
        this.submitQueue.clear();

and replace it with our reference.

        // Create View
        this.dropDownBoxView = new DropDownBoxView(this.store, this.el, this);

        // Subscribe master view to the store
        this.store.subscribe(this.dropDownBoxView.render.bind(this.dropDownBoxView));

        if (brease.config.isKeyboardOperationEnabled()) {
            // Update focus after a render of the view
            this.dropDownBoxView.addEventListener('ViewRendered', this.focusHandler.onRender.bind(this.focusHandler));
        }

The code for removing the DropDownBoxView was stolen from the dispose function from the original widget :wink:

Now that we injected our own DropDownBoxView we can finally make the code changes to modify the behavior of the DropDownBox list. Again, I am not listing all the changes, that is something you can easily do with a compare between the original and modified version. The important change is in the function “getLocation”. All I really do is modifing the top position of the list box.

        // Calculate top for left, right and center
        if (listPositionExt === listPositionExtDropUp) {
        location.top = targetRect.bottom - listRect.height * scaleFactor;
        } else if (listPositionExt === listPositionExtCenter) {
        location.top = (pageHeight - listRect.height * scaleFactor) / 2;
        // Limit list box top to parent top
        if (location.top < 0) {
            location.top = 0;
        }
        // If top of list box is lower than button top move it up to button top
        if (location.top > targetRect.top) {
            location.top = targetRect.top;
        }
        // If bottom of list box is higher than button bottom move it down
        if (location.top + listRect.height * scaleFactor < targetRect.bottom) {
            location.top = targetRect.bottom - listRect.height * scaleFactor;
        }
        } else {
        location.top = targetRect.top;
        }

And with this change you have a brand new DropDown, DropUp and DropCenter box. I will add the complete code to this post as reference.

Stephan
brXtended.zip (1.0 MB)

6 Likes

I would like to add that I’ve wrote a python script to copy paste with dependencies a brease widget to a custom widget library. It make the copied widget directly usable in the custom library and you can inherit from it too :slight_smile:

I didn’t had time to pack it to an executable so I just publish it on github as it is for the moment.

Here is the repo: GitHub - FBoissadier/WDTC_InherateWidget

Contributions are welcome! Feel free to submit issues or pull requests to improve the script

Regards,
Florent

2 Likes