Skip to Content
Technical Articles

UI5 and WebComponents working together

Intro

During last OpenUI5 conference, SAP confirmed that they are moving from MVC model to component infrastructure. One of the idea was to use WebComponents/Custom Elements and React Components. It’s HTML5 standard for defining new HTML elements. I had to chance know it better in 2014, when I played with polyfils.  It was quite different from what ruled on the market. In those days I used a lot JQuery and Angular 1.x with its magical binding, but unfortunately problematic performance. In this time Angular directives gave us possibility for some encapsulation, similar to WebComponents. WebComponents standard, to use capsulated/modularized DOM in shadow/light DOM (at beginning worked only in Chrome), was different but it was very needed for developing big front end applications.

One of the first frameworks that used Custom Elements was Polymer. In 2015, I introduced it with WebComponents on one of the open source conference, as our development future.

Now Custom Elements are the part of the Angular, Polymer, React, Vue, Lit Element, and many more.

All documentation can be find here:

https://developers.google.com/web/fundamentals/web-components/

https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements

https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events

UI5

For first time I wrote something in UI5  in 2015, when everything was in JS. In Angular or Polymer everything was way more natural (even if abstract) and more modern then in UI5. Frameworks had about 200-500 kb when UI5 had 1,5 mb. When people had limited network, it was a problem. Then SAP improve framework to use Component.js. SAP started to add async loaders to framework parts. XML views/fragments become very popular. I liked this solution, but still there wasn’t possibility write something reusal, in proper way. There was problem with global scope. I wanted to write more then view/controller. I tried to create own libs in my projects. From quite some time framework had a lot of improvements and its going in the really good direction. We can write reusable components that helps to have less code with more smarter solutions. During last few years I’ve seen a lot of positive changes. UI5 can be different at beginning, but when you understand what is behind, how the core is really working, a lot of things becomes clear.

From front end side I had chance to work also with Angular, Polymer, React, JQuery. That’s why I’m mixing different solutions, from different frameworks.

 

Table of Contents:

1. Component introduction – React High Order Component
2. Registering component as custom element
3. Connection between Custom Element and UI5 component
4. Updating Properties and Attributes
5. Component directory architecture – finally we have .component.xml with component content
6. Lifecycle methods from both UI5 and Custom Element
7. Attributes Observables
8. Shadow DOM and light DOM with UI5 WebComponent
9. No more !important – override css sap classes in shadow DOM
10. Slots and templates
11. “slotschange” event handler
12. Creating Custom Events, SAP EventBus, Component Events
13. And finally how to test – QUnit and OPA5 tests
14. To be continue – roadmap with this project
15. Project updates
Link to repository:

https://github.com/abagat0/UI5WebComponent

 

How it works:

1. Component introduction – React High Order Component

I based on React High Order Components (more info here https://reactjs.org/docs/higher-order-components.html) . It’s feature ,where I’m creating higher component, that manage other component, by changing behavior, depends of data/state that was send . In the past in React there was feature called mixin. Due to poor performance it was replaced by high order components.

This is React High Order Component implementation

// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
  // ...and returns another component...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }
    render() {
      // ... and renders the wrapped component with the fresh data!
      // Notice that we pass through any additional props
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

Bellow my UI5 Component.ts. Last lines  of render method are calling custom element instance and passing attributes with data ( similar to react in render function). I’m passing attributes separately to have observables for changes and simpler update. Only attributes that are declared in TestComponent class (in this case pro and message) will be attach to Custom Element. Additionally I’m passing componentId that join custom element instance with ui5 component.

export default class TestComponent extends UIWebComponent {
   public static metadata: any = {
       manifest:"json",
       properties :{
           pro : {type : "string", defaultValue : "{/message}", bindable: true},
           message : {type : "string", defaultValue : "{/message}", bindable: true}
        }
    };

    public init(): void {
        super.init.apply(this, arguments);
        this.setModel(new JSONModel({message: "start message"}))
    }

    public render(oRenderManager: RenderManager):void {

        // convenience variable
        const oRM = oRenderManager;
        const oComponent = this;
        oRM.write("<div ");
        oRM.write(oComponent.getId())
        oRM.writeClasses();
        oRM.write("><");

        // call custom element tag
        oRM.write(oComponent.getProperty("htmlTag"));

        // change propeties to attributes
        oComponent.writeProperties(oRM)

        // pass componentId, it's very important, without this
 //custom element and component cannot find/update each other
        oRM.writeAttribute("componentId",oComponent.getId())

        // pass resolver to begining template (if it was sth declared), 
//if there were slots then call slot=' slot name ' to navigate browser where place it
        oRM.write("><div slot='test' >"+
            oRM.getHTML(oComponent.oContent)+
            "</div><div slot='secondTest'>second</div></"+
            // close custom element tag
            oComponent.getProperty("htmlTag")+
            "></div></div>");
    };
}

 

Bellow alternatives in different frameworks:

SAPUI5 AnguarJs ReactJS WebComponents Polymer
 

MixInto, Interfaces

see website

 

Mixins:

see website

High order components:

see website

Mixins

see website

Mixins

see website

 

In my solution there are two classes. One is Test Component that is extending UIWebComponent, second is UI5CustomElement. When UI5 render component container, passing all settings that are needed to Custom Element, to behave in demanded way. “Higer Component” is from UI5. It’s managing of Custom Element. The main logic (extra properties, press handlers etc..) is written in Component.ts(Test Component).

2. Registering Custom Element in the browser

I added link to WebComponents in index.html page.

<script type="text/javascript" src="node_modules/@webcomponents/custom-elements/src/native-shim.js"></script>

If we want to support IE we have to add also links to shady DOM (see Browser Support and shadow DOM doc ).

UIWebcomponent class is responsible for creating Custom HTML Element. Webcomponent class can be defined in browser only once.

        // define custom element in browser
        try {
            customElements.define(customElTag, UI5CustomElement);
        } catch {
        }

 

3. Connection between Custom Element and UI5 Component

My Idea was to have strong connection between component and its Custom Element implementation. Both parties talk with each other. That’s helping with components updates. I joined them by componentId that I’m passing as the attribute.:

  • Component.ts knows which Custom Element is its html instance, because it is set in the whenDefined promise resolver, and saved to customElement property
customElements.whenDefined(customElTag).then(() => {
            let allInstances: Array<HTMLElement> = Array.from(document.querySelectorAll(customElTag));
            allInstances.map((item: HTMLElement) => {
                if (item.getAttribute("componentId") === this.getId()) {
                    this.customElement = item;
                }
            })
        });
  • UI5CustomElement knows which Component.ts is its logic, because it’s receiving component id in the attributes. It’s calling UI5 in constructor for component instance and save it own context property
 constructor() {
     this.context = sap.ui.getCore().getComponent(this.getAttribute("componentId"));
}

 

4. Updating Properties and Attributes

Component.ts properties setters are not only updating component props, but they are also updating attributes in Custom Element. SAP allows to override getters and setters (see more). That mean’s when property in model changed ( binding was attached to the property), or we’re changing property, all it is automatically reflected to Custom Element with method setAttribute:

   public setMessage(message:string):object{
        this.setProperty("message", message);
        this.setAttribute("message", message)
        return this;
    }

Here is the list of the properties:

Property pro and message are declared in Component.ts class. Rest is coming from UIWebComponent:

  • observedAttributes -attribute that changes are observed. We have to be careful to not observe all if is not needed. Each one create own events and has impact to performance.
  • shadowDom – if I want to render comp in shadow DOM, then I have to declare this property with type of the shadow DOM (open/closed). If is not declared it will be rendered in the light DOM
  • cssUrl – url to custom component css. This is helping with modularization of the css.
  • xmlTemplate – sap name for fragment with extension .component.xml. This will be rendered when custom element is called on html.
  • htmlTag – name of the html tag that will be representing custom element
<core:ComponentContainer 
name="ui5.testApp.components.TestComponent"
settings="{
pro:'{/message}', 
message: '{/message}', 
observedAttributes:['message'], 
shadowDom:'open',                                        
cssUrl:'components/TestComponent/TestComponent.component.css',
xmlTemplate:'ui5.testApp.components.TestComponent.TestComponent', 
htmlTag:'test-comp'}"
propagateModel="true"/>

 

5. Component directory architecture – finally we have .component.xml with component content

I really like solution in Angular that we have all component stuff together:

I added to UIWebComponent class feature, that accept fragments with component.xml extension.

For each component you can add separate css file which will help with modularization (it comes with WebComponents features).

So in summary that’s why I implemented:

 

6. Lifecycle methods from both UI5 and Custom Element

I added possibility to use all lifecycle methods from both technologies. You can use standard UI5 or you can take from Custom Element. In WebComponent class, I added reference to UI5 context. Enough to call it in Component.ts and all will be reflected.

SAPUI5

source (https://blogs.sap.com/2018/11/12/sapui5-controller-lifecycle-methods-explained/)

Custom Elements:

source (https://developers.google.com/web/fundamentals/web-components/customelements#reactions)

7. Attributes Observables

Custom Elements have possibility to observe attributes changes. Thanks to immediate updates of the attributes, I can observe and build my logic, for specific attribute. In component container settings I’m setting array, with names of observed attributes. Method attributeChangedCallback is called to resolve event, after attribute change.

Bellow similar functionalities across frameworks

SAPUI5 AngularJs ReactJS WebComponents Polymer

On change,

custom implementation

Simple change

State

change

Attributes change

Computed,

observer

properties

8. Shadow DOM and light DOM with UI5 WebComponent

Custom Elements have possibility to be render in light DOM or shadow DOM (see https://developers.google.com/web/fundamentals/web-components/shadowdom)

I attached to my solution possibility, to choose which one should be used. This is declared during class initialization. If there is no info in component container settings then custom element is using light dom.

Rendering in shadow dom: Open

Rendering in light DOM

 

9. No more !important – override css sap classes in shadow DOM

Shadow Dom has couple nice features. One of them is separation of css and js. If I want to render something in shadow DOM, I have to attach once again all css and js files, that were used for component. This is why I wrote method that check what sap libs that were loaded, attach css to Custom Element. This is happened only at the beginning.

// copy current loaded css links from ui5 config to custom element shadow dom
    public copyCurentSAPCss(): string {
        const loadedLibraries = Object.keys(sap.ui.getCore().getLoadedLibraries());
        let cssLinks = '';
        loadedLibraries.map((library: string) => {
            cssLinks += '<link rel="stylesheet" type="text/css" href="' +
                jQuery.sap.getResourcePath(library.replace(/\./g\ , "/"))+
                '/themes/' +
            sap.ui.getCore().getConfiguration().getTheme() + '/library.css">'
        })
        return cssLinks;
    }

In shadow DOM I had to copy also JS. I’m working on solution that will attach js files and manage events with proper scope.

Big advantage of shadow DOM is that, we can override sap base css class. Before we have to struggle. to have good selectors, to be treated by the browser with higher importance, or just use not suggested !important for new styles. Below example what the difference. Button with red font is in shadow dom. The other one is rendered with slot which comes from light dom.

Html look’s like this. Last file is with my new class style:

 

10. Slots and templates

Slots and templates are nice possibility in Custom Elements (see documentation , slots docs in developer mozilla) to add placeholders at start template. After that browser puts in those placeholders some elements. Slots are existing in the shadow DOM.

Graphic representation of the simple template with slots, looks like that:

<html tag> <html tag> <html tag> <slot name=”first” /> <html tag>
<html tag> <slot name=”second” /> <html tag> <html tag>  <html tag>

When custom element is called, I can attach to it html with elements, that are replacing slots:

<simple-custom-tag>
  <div>this is not slot</div>
  <div slot="second"> This is second slot</div>
  <div slot="first"> This is first slot</div>
  <div> last div</div>
</simple-custom-tag>

After rendering it will be look like this

<html tag> <html tag> <html tag> <html tag> This is first slot
<html tag> This is second slot <html tag> <html tag>  <html tag>
    This is not slot
    last div

In my solution, I’m creating that kind of template in onBefore rendering method:

this.template = document.createElement('template');
this.template.innerHTML = cssLinks + `
<div></div>
<slot name="test"></slot>                 
<slot name="secondTest"></slot>
<div>last</div>
`;

This is passed, as initial html to Shadow DOM. It will be covered by the next step, which is Component.ts rendering process.

I’m replacing slots by xml template:

 oRM.write("><div slot='test' >"+
            oRM.getHTML(oComponent.oContent)+
            "</div><div slot='secondTest'>second</div></"+
            // close custom element tag
            oComponent.getProperty("htmlTag")+
            "></div></div>");

If I want to attach slot names to controls, to render directly without div/span html, it should be first updated class sap.ui.core.Control, which is base for each control.

Html template tag is not supported by IE (see more). In the past I used simple script tag:

<script type="text/template" id="someIdToCatch"></script>

 

11. “slotschange” event handler

There is special event “slotchange” that I can attach to slot instance. In Component.ts, I’m adding Custom Element lifecycle method. I can attach event listener to this event.

 public connectedCallback(element: object):void{
        const slot = element.shadowRoot.querySelector('slot');
        slot.addEventListener('slotchange', e => {
            console.log('light dom children changed!');
        });
    }

 

12. Creating Custom Events, SAP EventBus, Component Events

WebComponents have standard event handlers (see more). We can create own Custom Events.

What are Events?

No matter in which technology, Events are incidents that are triggered, after f.e. button click. We can send with them data or just trigger another logic. There are always 2 sides of Events. One is publisher (which will be button that was clicked). Second one is subscription side (Event listeners). In UI5 we have EventBus (which is more like creator of Custom Events, see more) and EventProvider (for communication between controls) (see more).

We have to be aware that UI5 UIArea is taking care already of the standard events. In this situation we have to pay attention to not override something (if we don’t want to). When we’re working in shadow DOM,  events are working locally. If we want to send them to rest DOM children/components we have to attach flag composed:true.

new Event('test-comp-click', {bubbles: true, composed: true})

Here are example of event handling in different frameworks:

SAPUI5 AngularJs ReactJS WebComponents Polymer

EventBus

Event Provider

Standard Events (property change, click), EventEmitter Standard Events (state change, click) and EventEmitter StandardStandard Events (property change, click) and Custom Events Standard (f.e. property change, click) and  Custom Events

To work with Event Bus, I have to subscribe, to events that I want to follow, in init method.

To work with Custom Events with custom element, I have to use its connectedCallback method (see point 11).

Additionally in Component.ts metadata I can add list of events that can be used by the other parts of the application.  can subscribe to those events and receive some information from component (see component doc) and (see custom controls doc)

 

13. And finally how to test QUnit and OPA5 tests

Due to the fact that we are using all time UI5 as a higher component we don’t have to be worry about tests. Mostly everything is working as a standard UI5 app.

I had to attach to index.html link to WebComponents.

14. To be continue – roadmap with this project

  • attach JS files / sap control events registration in shadow DOM
  • dynamic css sap libs loader all the time for shadow DOM
  • binding component properties to xml fragment -> Composite support
  • writing test with typescript spec files

 

15. Project Updates:

Handling with Events.

UI5 is using JQuery.Events. UIArea (html document) is binding all events at the beginning. Because jQuery and shadowDom are totally historically/technologically in different place, one of the solution is creation shadow DOM listener handler. This is working as a host for shadowDom. I’m binding events listener for first div in shadow Dom. Now It listen click events. I’m resolving event in handleEvents method.

Communication between controls is working properly.

 

There was problem with elements selection in html page that are in shadowDom.

I had to prototype getDomRef method from Element class. All controls that are rendered in shadowDom have model with that contain component id. This method is called by all controls for every Dom update.

Element.prototype.getDomRef = function (sSuffix) {
    let DomRef = (((sSuffix ? this.getId() + "-" + sSuffix : this.getId())) ? window.document.getElementById(sSuffix ? this.getId() + "-" + sSuffix : this.getId()) : null);
    if (!DomRef && this.getModel("settingsModel")) {
        let oComponent = sap.ui.getCore().getComponent(this.getModel("settingsModel").oData.compId);
        if (sSuffix && oComponent.customElement) {
            DomRef = oComponent.customElement.shadowRoot.querySelector("#" + this.getId() + "-" + sSuffix);
        } else if (oComponent.customElement) {
            DomRef = oComponent.customElement.shadowRoot.querySelector("[id*='" + this.getId() + "']");
        }
    }
    return DomRef;
};

Css Links

All css libs are attaching automatically to all shadow dom templates.

Composite Support. We can bind model and component properties.

I added two aggregations:

LightDom and ShadowDom. Both of them have possibility to bind or models or properties from component. That’s why this code will be resolved properly:

// property binding
<Button text="{$this>xmlTemplate}" type="Transparent" press="handlePress" />
// standard model binding
<Text text="{/message}" />

This change had impact to component architecture. There are two additional properties:

  • xmlTemplateShadowDom (XML fragment) replace template (html created in JS)
  • xmlTemplateLightDom(XML fragment)

xmlTemplateShadowDom:

I switched from template created in component to xml fragment. Unfortuantelly xmls notation for html and core:html don’t support slots.I had to create two controls named Slot and Div that creating/ resolving slots:

<customControl:Div>
 <Button text="{$this>xmlTemplate}" type="Transparent" press="handlePress" />
 <Button text="{/message}" type="Transparent" press="handlePress" />
 <customControl:Slot name="test"></customControl:Slot>
 </customControl:Div>

Div is extension of FlexBox so has all features from this control, plus additionally bind slot name in html.

Slot is creating simple html slot with given name.

 

xmlTemplateLightDom :

This is part that will be rendered in light dom. In case that I want to resolve some slots created before, enough to use control Div with slot name, and everything will be attached properly by custom element.

<customControl:Div>
      <customControl:Div slot="test">
              <Button text="{/message}" type="Transparent" press="handlePress" />
              <Text text="{$this>template}" />
      </customControl:Div>
</customControl:Div>

 

Opa5 and QUnit test loaded dynamically with .spec.ts extension:

 

I changed architecture of the tests. Now All tests are in the files with .spec.ts extension and opa5|qunit mark. I don’t have to take care of attaching tests to test list…. I can just create test and lite-server will take care of it.

 

I setup node lite server to check all dir to search files with this extension.

/opa5 -> route for opa5 tests

Opa5 has two properties. Test is responsible for test modules, view is containing opa5 pages (taken from view dir)

AllOpa5.ts consume and run Opa5 tests:

Helper.getRequest('/opa5')
    .then(response => {
        const fileList: Opa5Files = JSON.parse(response);
        Opa5.extendConfig({
            arrangements: new Common(),
            viewNamespace: "ui5.testApp.view."
        });
        sap.ui.require(fileList.view, () => {
            sap.ui.require(fileList.test, () => {
                QUnit.start();
            })
        })
    })

/qunit -> route for unit tests

AllQunit.ts consume and run qunit tests:

Helper.getRequest('/qunit')
    .then(response => {
        const fileList: Array<string> = JSON.parse(response);
        sap.ui.require(fileList, () => {
            QUnit.start();
        })
    })

 

Spec files are compiled, after each change. I setup to ignore them by typescript. This can be change in tsconfig file, but before run, tests, have to be compiled.

 

Be the first to leave a comment
You must be Logged on to comment or reply to a post.