All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.flow.data.renderer.LitRenderer Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2000-2024 Vaadin Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.vaadin.flow.data.renderer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.data.provider.DataGenerator;
import com.vaadin.flow.data.provider.DataKeyMapper;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.function.SerializableBiConsumer;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.function.ValueProvider;
import com.vaadin.flow.internal.JsonSerializer;
import com.vaadin.flow.internal.JsonUtils;
import com.vaadin.flow.internal.UsageStatistics;
import com.vaadin.flow.internal.nodefeature.ReturnChannelMap;
import com.vaadin.flow.internal.nodefeature.ReturnChannelRegistration;
import com.vaadin.flow.shared.Registration;

import elemental.json.JsonArray;
import elemental.json.JsonObject;

/**
 * LitRenderer is a {@link Renderer} that uses a Lit-based template literal to
 * render given model objects in the components that support the JS renderer
 * functions API. Mainly it's intended for use with {@code Grid},
 * {@code ComboBox} and {@code VirtualList}, but is not limited to these.
 *
 * @author Vaadin Ltd
 * @since 22.0.
 *
 * @param 
 *            the type of the model object used inside the template expression
 *
 * @see #of(String)
 * @see https://lit.dev/docs/templates/overview/
 * @see <vaadin-combo-box>.renderer
 */
@JsModule("./lit-renderer.ts")
public class LitRenderer extends Renderer {

    static {
        UsageStatistics.markAsUsed("flow-components/LitRenderer", null);
    }

    private final String templateExpression;

    private final String propertyNamespace;

    private final Map> valueProviders = new HashMap<>();
    private final Map> clientCallables = new HashMap<>();

    private final String ALPHANUMERIC_REGEX = "^[a-zA-Z0-9]+$";

    private LitRenderer(String templateExpression) {
        this.templateExpression = templateExpression;

        int litRendererCount = 0;
        if (UI.getCurrent() != null) {
            // Generate a unique (in scope of the UI) namespace for the renderer
            // properties.
            litRendererCount = UI.getCurrent().getElement()
                    .getProperty("__litRendererCount", 0);
            UI.getCurrent().getElement().setProperty("__litRendererCount",
                    litRendererCount + 1);

        }
        propertyNamespace = "lr_" + litRendererCount + "_";
    }

    LitRenderer() {
        this("");
    }

    /**
     * Creates a new LitRenderer based on the provided template expression. The
     * expression accepts content that is allowed inside JS template literals,
     * and works with the Lit data binding syntax.
     * 

* The template expression has access to: *

    *
  • {@code item} the model item being rendered
  • *
  • {@code index} the index of the current item (when rendering a * list)
  • *
  • {@code item.property} any property of the model item exposed via * {@link #withProperty(String, ValueProvider)}
  • *
  • any function exposed via * {@link #withFunction(String, SerializableConsumer)}
  • *
*

* Examples: * *

     * {@code
     * // Prints the `name` property of a person
     * LitRenderer. of("
Name: ${item.name}
") * .withProperty("name", Person::getName); * * // Prints the index of the item inside a repeating list * LitRenderer.of("${index}"); * } *
* * @param * the type of the input object used inside the template * * @param templateExpression * the template expression used to render items, not * null * @return an initialized LitRenderer * @see LitRenderer#withProperty(String, ValueProvider) * @see LitRenderer#withFunction(String, SerializableConsumer) */ public static LitRenderer of(String templateExpression) { Objects.requireNonNull(templateExpression); return new LitRenderer<>(templateExpression); } @Override public Rendering render(Element container, DataKeyMapper keyMapper, String rendererName) { DataGenerator dataGenerator = createDataGenerator(); Registration registration = createJsRendererFunction(container, keyMapper, rendererName); return new Rendering() { @Override public Optional> getDataGenerator() { return Optional.of(dataGenerator); } @Override public Registration getRegistration() { return registration; } }; } private void setElementRenderer(Element container, String rendererName, String templateExpression, ReturnChannelRegistration returnChannel, JsonArray clientCallablesArray, String propertyNamespace) { container.executeJs( "window.Vaadin.setLitRenderer(this, $0, $1, $2, $3, $4)", rendererName, templateExpression, returnChannel, clientCallablesArray, propertyNamespace); } /** * Returns the Lit template expression used to render items. * * @return the template expression */ protected String getTemplateExpression() { return templateExpression; } /** * Returns the namespace used to prefix property names when sending them to * the client as part of an item. * * @return the property namespace */ String getPropertyNamespace() { return propertyNamespace; } private Registration createJsRendererFunction(Element container, DataKeyMapper keyMapper, String rendererName) { ReturnChannelRegistration returnChannel = container.getNode() .getFeature(ReturnChannelMap.class) .registerChannel(arguments -> { // Invoked when the client calls one of the client callables String handlerName = arguments.getString(0); String itemKey = arguments.getString(1); JsonArray args = arguments.getArray(2); SerializableBiConsumer handler = clientCallables .get(handlerName); SOURCE item = keyMapper.get(itemKey); if (item != null) { handler.accept(item, args); } }); JsonArray clientCallablesArray = JsonUtils .listToJson(new ArrayList<>(clientCallables.keySet())); List registrations = new ArrayList<>(); // Since the renderer is set manually on the client-side, an attach // listener for the host component is required so that the renderer gets // applied even when the host component gets a new Web Component // instance (for example on detach + reattach). // // The attach listener needs to be released when the Renderer instance // is no longer used so the registration is cleared by the renderer // registration. registrations.add(container.addAttachListener(e -> { setElementRenderer(container, rendererName, getTemplateExpression(), returnChannel, clientCallablesArray, propertyNamespace); })); // Call once initially if (container.getNode().isAttached()) { setElementRenderer(container, rendererName, getTemplateExpression(), returnChannel, clientCallablesArray, propertyNamespace); } // Get the renderer function cleared when the LitRenderer is // unregistered registrations.add(() -> container.executeJs( "window.Vaadin.unsetLitRenderer(this, $0, $1)", rendererName, propertyNamespace)); return () -> registrations.forEach(Registration::remove); } private DataGenerator createDataGenerator() { // Use an anonymous class instead of Lambda to prevent potential // deserialization issues when used with Grid // see https://github.com/vaadin/flow-components/issues/6256 return new DataGenerator() { @Override public void generateData(SOURCE item, JsonObject jsonObject) { valueProviders.forEach((key, provider) -> { jsonObject.put( // Prefix the property name with a LitRenderer // instance specific namespace to avoid property // name clashes. // Fixes https://github.com/vaadin/flow/issues/8629 // in LitRenderer propertyNamespace + key, JsonSerializer.toJson(provider.apply(item))); }); } }; } /** * Makes a property available to the template expression. Each property is * referenced inside the template by using the {@code ${item.property}} * syntax. *

* Examples: * *

     * {@code
     * // Regular property
     * LitRenderer. of("
Name: ${item.name}
") * .withProperty("name", Person::getName); * * // Property that uses a bean. Note that in this case the entire "Address" object will be sent to the template. * // Note that even properties of the bean which are not used in the template are sent to the client, so use * // this feature with caution. * LitRenderer. of("Street: ${item.address.street}") * .withProperty("address", Person::getAddress); * * // In this case only the street field inside the Address object is sent * LitRenderer. of("Street: ${item.street}") * .withProperty("street", person -> person.getAddress().getStreet()); * } *
* * Any types supported by the {@link JsonSerializer} are valid types for the * LitRenderer. * * @param property * the name of the property used inside the template expression, * not null * * @param provider * a {@link ValueProvider} that provides the actual value for the * property, not null * @return this instance for method chaining */ public LitRenderer withProperty(String property, ValueProvider provider) { Objects.requireNonNull(property); Objects.requireNonNull(provider); valueProviders.put(property, provider); return this; } /** * Adds a function that can be called from within the template expression. *

* Examples: * *

     * {@code
     * // Standard event
     * LitRenderer.of("")
     *          .withFunction("handleClick", object -> doSomething());
     * }
     * 
* * The name of the function used in the template expression should be the * name used at the functionName parameter. This name must be a valid * JavaScript function name. * * @param functionName * the name of the function used inside the template expression, * must be alphanumeric and not null, must not be * one of the JavaScript reserved words * (https://www.w3schools.com/js/js_reserved.asp) * @param handler * the handler executed when the function is called, not * null * @return this instance for method chaining * @see https://lit.dev/docs/templates/expressions/#event-listener-expressions */ public LitRenderer withFunction(String functionName, SerializableConsumer handler) { return withFunction(functionName, (item, ignore) -> handler.accept(item)); } /** * Adds a function that can be called from within the template expression. * The function accepts arguments that can be consumed by the given handler. * *

* Examples: * *

     * {@code
     * // Standard event
     * LitRenderer.of("")
     *          .withFunction("handleClick", item -> doSomething());
     *
     * // Function invocation with arguments
     * LitRenderer.of(" handleKeyPress(e.key)}>")
     *          .withFunction("handleKeyPress", (item, args) -> {
     *              System.out.println("Pressed key: " + args.getString(0));
     *          });
     * }
     * 
* * The name of the function used in the template expression should be the * name used at the functionName parameter. This name must be a valid * Javascript function name. * * @param functionName * the name of the function used inside the template expression, * must be alphanumeric and not null, must not be * one of the JavaScript reserved words * (https://www.w3schools.com/js/js_reserved.asp) * @param handler * the handler executed when the function is called, not * null * @return this instance for method chaining * @see https://lit.dev/docs/templates/expressions/#event-listener-expressions */ public LitRenderer withFunction(String functionName, SerializableBiConsumer handler) { Objects.requireNonNull(functionName); Objects.requireNonNull(handler); if (!Pattern.matches(ALPHANUMERIC_REGEX, functionName)) { throw new IllegalArgumentException( "Function name must be alphanumeric"); } clientCallables.put(functionName, handler); return this; } /** * Gets the property mapped to {@link ValueProvider}s in this renderer. The * returned map is immutable. * * @return the mapped properties, never null */ public Map> getValueProviders() { return Collections.unmodifiableMap(valueProviders); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy