Dynamic HTML using a Vaadin LitRenderer
Asked Answered
B

1

5

Using Vaadin 22+

I'm trying to display some dynamic html in a Vaadin Grid, using a LitRenderer.

This could previously be achieved using the now-deprecated TemplateRenderer using this hack

var templateRenderer = TemplateRenderer.<Event>of("""
<div>
  <ul inner-h-t-m-l="[[item.html]]">
  
  </ul>
</div>
""").withProperty("html", item -> {
  String listItems = "";
  for (EventItem eventItem : item.getEventItems()) {
    listItems += "<li>"+eventItem.getValue()+"</li>";
  }
  return listItems;
});

/*
  Produces html like
  <div>
    <ul>
      <li>Thing 1</li>
      <li>Thing 3</li>
      <li>Thing 845</li>
    </ul>
  </div>
  */

The important part here was setting the innerHTML of the wrapping element.

Is there a way to do this using the LitRenderer? I see that Lit-Template itself has the unsafeHTML directive for just this kind of purpose. But I've had no luck trying to get it or another method working.

Thanks

Updated: 2021.3.2 to fix the example code

Boar answered 1/3, 2022 at 1:33 Comment(0)
P
14

You can use a lit renderer without unsafeHTML to render your example:

grid.addColumn(LitRenderer.<String>of(" <ul>" +
                "        ${item.eventItems.map((eventItem) =>" +
                "            html`<li>${eventItem}</li>`\n" +
                "        )}\n" +
                "    </ul>").withProperty("eventItems", i -> {
                    return List.of("item 1", "item 2", "item 3");
        }));

If you really want to display custom html, you can use a Component Renderer:

grid.addColumn(new ComponentRenderer<>(i -> {
            List<String> strings = List.of("item 1", "item 2", "item 3");
            String listItems = "";
            for (String eventItem : strings) {
                listItems += "<li>"+eventItem +"</li>";
            }
            return new Html("<ul>"+listItems+"</ul>");
        }));

And if you don't want to use a Component Renderer you can create a custom lit component with unsafeHTML:

import {css, html, LitElement} from 'lit';
import {unsafeHTML} from 'lit/directives/unsafe-html.js';
import { customElement } from 'lit/decorators.js';

@customElement('unsafe-html-component')
export class MyTestComponent extends LitElement {

    static get styles() {
        return css`
        :host {
            display: block;
        }
        `;
    }
    private html:string = "";

    render() {
        return html`<ul>${unsafeHTML(this.html)}</ul>`;
    }

}

And you can use it in Java:

grid.addColumn(LitRenderer.<String>of(" <unsafe-html-component .html=${item.html}></unsafe-html-component>")
                .withProperty("html", i -> {
                    List<String> strings = List.of("item 1", "item 2", "item 3");
                    String listItems = "";
                    for (String eventItem : strings) {
                        listItems += "<li>"+eventItem +"</li>";
                    }
                    return listItems;
        }));

EDIT: There is also a fourth solution:

grid.addColumn(LitRenderer.<String>of(" <ul .innerHTML=${item.html}></ul>")
                .withProperty("html", i -> {
                    List<String> strings = List.of("item 1", "item 2", "item 3");
                    String listItems = "";
                    for (String eventItem : strings) {
                        listItems += "<li>"+eventItem +"</li>";
                    }
                    return listItems;
        }));

Here is a full example with the 4 ways to display your example without the TemplateRenderer:

import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.data.renderer.LitRenderer;
import com.vaadin.flow.router.Route;

import java.util.ArrayList;
import java.util.List;
@JsModule("./unsafe-html-component.ts")
@Route("template-grid")
public class GridView extends VerticalLayout {

    public GridView() {
        Grid<String> grid = new Grid<>();
        grid.addColumn(LitRenderer.<String>of(" <ul>" +
                "        ${item.eventItems.map((eventItem) =>" +
                "            html`<li>${eventItem}</li>`\n" +
                "        )}\n" +
                "    </ul>").withProperty("eventItems", i -> {
                    return List.of("item 1", "item 2", "item 3");
        }));
        grid.addColumn(LitRenderer.<String>of(" <unsafe-html-component .html=${item.html}></unsafe-html-component>")
                .withProperty("html", i -> {
                    List<String> strings = List.of("item 1", "item 2", "item 3");
                    String listItems = "";
                    for (String eventItem : strings) {
                        listItems += "<li>"+eventItem +"</li>";
                    }
                    return listItems;
        }));
        grid.addColumn(new ComponentRenderer<>(i -> {
            List<String> strings = List.of("item 1", "item 2", "item 3");
            String listItems = "";
            for (String eventItem : strings) {
                listItems += "<li>"+eventItem +"</li>";
            }
            return new Html("<ul>"+listItems+"</ul>");
        }));
        grid.addColumn(LitRenderer.<String>of(" <ul .innerHTML=${item.html}></ul>")
                .withProperty("html", i -> {
                    List<String> strings = List.of("item 1", "item 2", "item 3");
                    String listItems = "";
                    for (String eventItem : strings) {
                        listItems += "<li>"+eventItem +"</li>";
                    }
                    return listItems;
        }));
        add(grid);

        List<String> strings = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            strings.add(i + "");
        }

        grid.setItems(strings);
        grid.setSizeFull();
        setSizeFull();
    }
}

There is a ticket on github with more solutions: https://github.com/vaadin/flow-components/issues/2753

Paulina answered 2/3, 2022 at 7:51 Comment(5)
While non of those options are as simple and straight forward as I would have liked, I still upvoted because these are the options we currently have and you described them well. ComponentRenderer has a bigger overhead than the LitRenderer, so the latter should generally be preferred.Olivette
The ComponentRenderer in that case is as performant as the others since you only render one component (Html). So that shouldn't be a concern. Of course if you render multiple Java component the ComponentRenderer is slower than the other.Paulina
Alright. There's still the memory overhead of keeping the Html objects, but it would only be (more or less) for the visible rows, so that should be okay. But doesn't the Jsoup parser used by Html add any measurable overhead?Olivette
To measure the difference of performance, I've just created 3 grids of 100 columns of each renderer. And the result is basically the same (measure in Chrome devtools). The Jsoup parser for that small amount of Html is not really an issue. For a Component renderer or the unsafeHtml, the server is sending the entire Html (for each cell). For the lit renderer, the server is sending only the data per cell + the template per column (and the client side has to render it). If you have real data, you can test the 3 options and keep the best for you. (IMO unsafeHtml is not good for maintainability).Paulina
Thank you for your analysis. The Html component is definitely the most convenient option; and should also be safe if the values are escaped. unsafeHTML seemed sketchy from the start, but still interesting to see how everything works together.Olivette

© 2022 - 2024 — McMap. All rights reserved.