Extending not extendable Vaadin components

2 hours ago 1

Index

After quite a long time I ended up working on an application with Vaadin again, but this time not Vaadin 8, but Vaadin 24. Or to be exact, a migration from Vaadin 8 to Vaadin 24. For those not knowing, Vaadin is a Java GUI framework which allows you to use Java components to create Web-UIs.

As usually, I ended up requiring more functionality than the default components provided (especially frustrating when Vaadin 8 had these features but they were dropped), one of these was that I required a toolbar coupled to a Grid to house additional functionality and features. But how do you extend a component that is not made to be extended? The answer is: With a lot of swear words, some duct-tape, and one pretty gruesome hack.

But why?

As it turned out, I needed a toolbar along with every grid to house various control elements to interact with the Grid. There are now several approaches available as follows.

Extending the Grid and adding those components.

That's not how Vaadin components work. You can extend a Grid easily enough, but you can't add arbitrary components to it. Grid isn't a container, and adding them to the main element directly

getElement().appendChild(toolbarContainer);

will add the toolbarContainer as a child in the HTML hierarchy, but given that the Grid uses a Shadow DOM for its inner workings, the element will not be rendered (because it is not in the Shadow DOM).

<vaadin-grid style="flex-grow: 1; touch-action: none;" multi-sort-priority="prepend"> #shadow-root <style> /* SNIP */</style> <div id="scroller" style="touch-action: none;"> <table id="table" role="treegrid" aria-multiselectable="false" tabindex="0" aria-rowcount="6" aria-colcount="4"> SNIP </table> <div part="reorder-ghost"></div> </div> #end-shadow-root <div class="toolbar-container">SNIP</div> </div> </vaadin-grid>

You can't retrieve the Shadow DOM element either, as getting the ShadowRoot from the appropriate property of the Grid Element will only return an empty Optional.

@Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); System.out.println(getElement().getShadowRoot()); // Optional.empty }

So no adding elements directly there either.

As an anecdote, in Swing everything can contain child components, which is a neat feature that would have come in handy here.

External components

We could create an additional component which we then "associate" with the Grid in the view.

GridToolbar gridToolbar = new GridToolbar(); Grid<Item> grid = new Grid<>(); gridToolbar.setGrid(grid); VerticalLayout gridContainer = new VerticalLayout(); gridContainer.add(gridToolbar, grid);

That is possible but is unyieldy, and means that if we want to access internals of the Grid from within GridToolbar, we'll have to jump through hoops. Most important, this would require large changes in the views which are using the Grids.

Wrapping the Grid

We could also wrap the Grid.

public class ToolbaredGrid<BEAN_TYPE> extends HorizontalLayout { protected Grid<BEAN_TYPE> grid = null; public ToolbaredGrid() { super(); this.grid = new Grid(); // TODO Initialize toolbar element; add(toolbarContainer, grid); } public Grid<BEAN_TYPE> getGrid() { return grid; } }

That has similar problems as using an external component when it comes to accessing Grid functionality, and it means we need to split the API between the "wrapper" and the "real" Grid:

grid.performSomeAction(); grid.getRealGrid().doSomeOtherAction();

Which, again, is rather unwieldy and ugly. "Forwarding" to the "real" grid would be possible, but very cumbersome to write.

Now if all we need is a toolbar above (below?) the Grid, we could use a HeaderRow. We can join all cells together and we'll have a long space that we can use.

HeaderRow toolbarRow = prependHeaderRow(); toolbarRow.join(toolbarRow.getCells());

That comes with a completely different set of problems. If you have many columns, it will also scroll horizontally with the rest of the table, potentially pushing your controls out of view.

If you're using a FooterRow, you must add two, because the first one is not allowed to have joined columns (and you can't hide the first row either). Even better, calling removeAllFooterRows afterwards will throw an Exception, because removing the first will make the second row the first one, but which has joined cells, and the first one is not allowed to have joined cells.

FooterRow requiredEmptyFirstFooter = appendFooterRow(); FooterRow toolbarFooterRow = appendFooterRow(); toolbarFooterRow.join(toolbarFooterRow.getCells()); removeAllFooterRows(); // java.lang.UnsupportedOperationException: Top-most footer row cannot have joined cells.

But most important in this case, using setFrozen on columns with this will glitch the whole Grid and it will not render properly anymore. The horizontal scrollbar will glitch and you can scroll endlessly without the Grid updating the view.

So no joy there, either.

JavaScript based Shadow DOM injection

To get around all these limitations, we must be able to add elements to the Shadow DOM of the Grid. As it turns out, the Shadow DOM is accessible from the JavaScript side.

@Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); getElement().executeJs("console.error(this.shadowRoot);"); // ShadowRoot { } }

That will show the Shadow DOM element in the browser console. That means, if we can access it by executing JavaScript from the server-side, we can attach elements to it at the client-side from the server-side. So...we are done. That's it. That's the solution. Not what we all wanted, but what we've got.

Moving elements into the Shadow DOM

In order to utilize this, we must have elements that we can move to (or "inject" into) the Shadow DOM of the component. We can simply add those to the Element of the Component and then move them through JavaScript. For ease of use, they must be selectable in one way or another, I've chosen to use a custom class.

Element dummyElement = new Element("dummy"); dummyElement.addClassName("shadow-injection-child"); getElement().appendChild(dummyElement);

Then, onAttach we can run a JavaScript script which moves these to the Shadow DOM of the parent.

@Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); getElement().executeJs(readJavaScriptScriptContent()); }
let shadowRoot = this.shadowRoot; for (let shadowInjectionChildElement of this.querySelectorAll(".shadow-injection-child")) { shadowRoot.appendChild(shadowInjectionElement); }

And now our element has been moved from the Components root to the Shadow DOM root, where it is now displayed.

Styling

If we can attach arbitrary elements into the Shadow DOM, we can do that with custom styling, too. Nothing stops us from creating an Element with the "style" tag and attach it there.

Element customStyleElement = new Element("style"); customStyleElement.setProperty( "innerHTML", "* { background-color: red !important; }"); customStyleElement.addClassName("shadow-injection-child");

We can now style the Grid, too!

Dynamically, even

Given that we "just" relocate the client-side elements, we can still manipulate the Elements from the server-side as usual. For example, we could use it to inject dynamic styling to the Components that are extended this way. Assuming we have a custom style Element.

Element customStyleElement = new Element("style"); customStyleElement.setProperty( "innerHTML", "* { background-color: red !important; }"); customStyleElement.addClassName("shadow-injection-child");

We can simply update its content on a trigger.

customStyleElement.setProperty( "innerHTML", "* { background-color: green !important; }");

And it will be propagated correctly to the client-side element.

Demo

There's a simple demo project available at Codeberg, the vaadin-extending-components-demo.

A screenshot of the demo, showing an extended Grid component with custom styling and a toolbar attached to it. A screenshot of the demo, showing an extended Grid component with custom styling and a toolbar attached to it.

I wasn't going for style or functionality, but just as a demonstration with some nice-to have utilities. So please forgive the color choices and non-functional buttons.

Usage

The demo utilizes the ShadowInjector helper class, which has two main functions:

  1. Preparing elements for injection.
  2. Performing the injection.

So if we want to add our style Element from above, we need to create it and then have it prepared.

Element customStyleElement = new Element("style"); customStyleElement.setProperty( "innerHTML", "* { background-color: red !important; }"); ShadowInjector.addForInjection(this, customStyleElement);

That will perform all necessary steps (adding a class, adding the Element) for us.

In onAttach we must then call the main logic for injecting the previously added Elements.

@Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); ShadowInjector.injectChilds(this); }

This will automatically move the previously added and prepared childs into the Shadow DOM of the target Component.

Conclusion

The ability to build custom and reusable components is extremely important, not only for developers of libraries and frameworks, but for any software product. ERP applications, for example, always have special requirements, and more often than not they require the same functionality and features in dozens, hundreds, or even thousands of forms and screens. Being able to create custom components which contain much, if not all, of these features is very important to be able to provide a maintainable and stable application.

Read Entire Article