Élément composite
La classe ElementComposite enveloppe un élément HTML personnalisé ou un web component. Elle lie votre classe Java à l'élément Element sous-jacent et vous permet de travailler avec les propriétés, attributs et événements de cet élément via Java. Utilisez-la lors de l'intégration de web components dans une application webforJ.
ElementCompositeUtilisez ElementComposite lorsque vous enveloppez un web component tiers que webforJ ne fournit pas déjà. Si un composant intégré de webforJ couvre le cas d'utilisation (TextField, ColorField, Button, etc.), utilisez celui-ci. Pour des travaux DOM uniques qui n'ont pas besoin d'être réutilisés, la classe Element peut être utilisée directement sans wrapper.
Ce guide démontre comment implémenter le web component de temps relatif Web Awesome en utilisant la classe ElementComposite.
Afficher le code
- RelativeTimeView.java
Annotations de classe
Trois annotations apparaissent couramment au sommet d'une sous-classe d'ElementComposite : @NodeName déclare la balise HTML que le composant enveloppe, et @JavaScript et @StyleSheet chargent tous les actifs côté client dont dépend le web component sous-jacent. @NodeName est requise et spécifique à ElementComposite. @JavaScript et @StyleSheet sont des annotations d'actifs webforJ générales et fonctionnent sur n'importe quelle classe, y compris les vues, les composants ou la classe App.
@NodeName
L'annotation @NodeName déclare la balise HTML que le composant enveloppe. webforJ utilise ce nom lors de la création de l'élément sous-jacent dans le DOM.
@NodeName("wa-relative-time")
public class RelativeTime extends ElementComposite {
// ...
}
Le nom de la balise doit correspondre à l'élément personnalisé enregistré côté client. Sans cette annotation, le cadre ne peut pas déterminer quel élément créer.
À l'intérieur d'une sous-classe, getNodeName() lit la balise déclarée, et getElement() retourne l'Element sous-jacent afin que vous puissiez appeler directement des méthodes de niveau DOM sur celui-ci.
@JavaScript
L'annotation @JavaScript charge le script qui définit ou enregistre le web component sous-jacent. Placez-la sur la classe afin que le script ne se charge que lorsque le composant est utilisé.
@NodeName("wa-relative-time")
@JavaScript("https://ka-f.webawesome.com/webawesome@3.12.0/webawesome.loader.js")
public class RelativeTime extends ElementComposite {
// ...
}
Plusieurs annotations @JavaScript sont autorisées, et webforJ déduplique automatiquement les chargements. Le même script ne se chargera pas deux fois si plusieurs composants en dépendent.
Voir Importation de fichiers JavaScript pour l'ensemble complet d'options, y compris top, attributes, et le timing de chargement.
@StyleSheet
L'annotation @StyleSheet charge un fichier CSS dont le composant dépend. Elle est utile pour les composants tiers qui expédient une feuille de style séparée, ou pour lier le style spécifique du composant avec le wrapper.
@StyleSheet("https://ka-f.webawesome.com/webawesome@3.12.0/styles/themes/default.css")
Pour les actifs localement empaquetés, utilisez le préfixe ws:// pour référencer des fichiers dans resources/static :
@StyleSheet("ws://components/relative-time.css")
Voir Importation de fichiers CSS pour l'ensemble complet d'options.
Descripteurs de propriétés et d'attributs
Les propriétés et attributs représentent l'état d'un web component, tenant généralement des données ou de la configuration. ElementComposite expose les deux via PropertyDescriptor.
Deux méthodes de fabrication sur PropertyDescriptor produisent le descripteur lui-même, une par cible de liaison :
PropertyDescriptor<T> property = PropertyDescriptor.property(String name, T defaultValue);
PropertyDescriptor<T> attribute = PropertyDescriptor.attribute(String name, T defaultValue);
PropertyDescriptor.property() se lie à une propriété JavaScript sur le nœud DOM. PropertyDescriptor.attribute() se lie à un attribut HTML. Le premier argument est le nom attendu par le web component. Le second est une valeur par défaut, qui fixe également le type Java du descripteur.
Déclarez le descripteur comme un champ privé sur le composant, puis lisez et écrivez à l'aide de set(PropertyDescriptor<V> property, V value) et get(PropertyDescriptor<V> property).
Les propriétés sont un état interne sur le nœud DOM et ne se reflètent pas dans le balisage. Les attributs sont le balisage HTML, visible pour les scripts et CSS externes.
// Exemple de propriété appelée "title" dans une classe ElementComposite
private final PropertyDescriptor<String> title = PropertyDescriptor.property("title", "");
// Exemple d'attribut appelé "value" dans une classe ElementComposite
private final PropertyDescriptor<String> value = PropertyDescriptor.attribute("value", "");
//...
set(title, "Mon Titre");
set(value, "Ma Valeur");
Les appels ci-dessus utilisent set() directement pour montrer la forme primitive. En pratique, set() et get() sont des méthodes protected sur ElementComposite. Elles constituent la couche primitive qui synchronise les valeurs Java avec l'élément sous-jacent, pas l'API publique que les consommateurs appellent. Le modèle prévu est de garder le PropertyDescriptor privé et d'écrire des méthodes publiques setX() et getX() qui délèguent aux primitives.
@NodeName("my-card")
public class Card extends ElementComposite {
private final PropertyDescriptor<String> heading =
PropertyDescriptor.property("heading", "");
public Card setHeading(String value) {
set(heading, value); // primitive protégée
return this;
}
public String getHeading() {
return get(heading); // primitive protégée
}
}
Un seul appel à set(descriptor, value) fait trois choses à la fois. Il pousse la valeur au client via setProperty() pour les propriétés, ou setAttribute() pour les attributs. Il stocke la valeur dans un cache local côté serveur, une map par instance de composant. Et il enregistre le type d'exécution aux côtés de la valeur, de sorte que les appels get() suivants sachent comment désérialiser.
Ce cache local est la raison pour laquelle get() peut être peu coûteux par défaut. get(descriptor) retourne la valeur mise en cache à partir du magasin côté serveur sans appel réseau, car chaque set() garde le cache synchronisé avec le client. Le second argument optionnel boolean contrôle s’il faut ignorer le cache et lire depuis le navigateur à la place.
String cached = get(heading); // lit à partir du cache côté serveur
String live = get(heading, true); // force une lecture depuis le navigateur
Définissez fromClient sur true lorsque la valeur peut changer sur le client sans que le serveur soit averti, comme une valeur de <input> tapée. Pour les propriétés pilotées par le serveur, la valeur par défaut évite un aller-retour.
Le troisième argument optionnel est un java.lang.reflect.Type et contrôle comment le résultat est désérialisé. webforJ résout le type dans cet ordre : l'argument Type explicite s'il est passé, puis le type d'exécution enregistré par un précédent set() sur le même descripteur, puis Object.class. En pratique, le type enregistré par un précédent set() est suffisant, donc le troisième argument peut généralement être omis. Il est nécessaire lorsque la classe enregistrée perd des informations dont le désérialiseur dépend, comme un type paramétré tel que List<String> dont la classe d'exécution est juste ArrayList.
La démo ci-dessous ajoute des propriétés pour le temps relatif basé sur la documentation du web component et les expose via des accesseurs et des mutateurs. Chaque ligne dans le fil d'activité utilise des valeurs différentes de format et numeric pour montrer comment le même composant se rend sous des configurations variées.
Afficher le code
- RelativeTimePropertiesView.java
- activityfeed.css
Propriétés contre attributs
Bien que PropertyDescriptor.property() et PropertyDescriptor.attribute() semblent interchangeables, elles ciblent différentes parties de l'élément sous-jacent. Choisir le mauvais entraîne des valeurs qui échouent silencieusement à s'appliquer.
Les propriétés sont des propriétés d'objet JavaScript sur le nœud DOM. Elles peuvent contenir n'importe quel type, y compris des chaînes, des booléens, des nombres, des objets et des tableaux, et elles représentent l'état d'exécution actuel de l'élément. Définir une propriété est un affectation directe JavaScript.
Les attributs sont du balisage HTML. Ils résident sur la balise d'ouverture de l'élément, sont toujours des chaînes, et représentent la configuration initiale de l'élément. Définir un attribut déclenche une mutation du DOM et une conversion de chaîne.
Dans certains cas, les deux restent synchronisés. Dans d'autres, elles divergent. La valeur de <input> est l'exemple classique : l'attribut value est la valeur initiale, tandis que la propriété value est la valeur actuelle que l'utilisateur a tapée. Lire l'attribut après que l'utilisateur ait tapé renvoie le balisage original, mais lire la propriété renvoie le contenu actuel du champ.
Utilisez les propriétés pour :
- État d'exécution changeant fréquemment : compteurs, sélections actuelles, valeurs tapées
- Types non-chaine : booléens, nombres, objets, tableaux
- Mises à jour sensibles aux performances : les propriétés évitent la conversion de chaîne requise pour les attributs
Utilisez les attributs pour :
- Configuration initiale : paramètres que le composant lit une fois lorsqu'il se connecte
- Sélecteurs CSS : valeurs que vous souhaitez cibler avec des sélecteurs tels que
[disabled]ou[variant="danger"] - Crochets d'accessibilité :
aria-label,role, et d'autres attributs ARIA - Paramètres de type chaîne qui changent rarement
Lors de l'enveloppement d'un web component tiers, consultez la documentation du composant pour confirmer quel nom correspond à une propriété et lequel à un attribut. Utiliser PropertyDescriptor.attribute() pour quelque chose que le composant expose uniquement comme propriété ne fonctionnera pas, et c'est vrai dans l'autre sens. Le composant ignorera silencieusement la valeur.
Typage des propriétés
Un descripteur est paramétré par le type Java de sa valeur. La syntaxe complète de déclaration est :
private final PropertyDescriptor<T> name =
PropertyDescriptor.property(String name, T defaultValue);
Le paramètre générique <T> déclare le type de la valeur. Le type d'exécution de la valeur par défaut fixe également T, donc l'argument générique a rarement besoin d'être spécifié explicitement. webforJ utilise T pour sérialiser et désérialiser les valeurs lors de la communication avec le client.
private final PropertyDescriptor<String> label =
PropertyDescriptor.property("label", "");
private final PropertyDescriptor<Boolean> disabled =
PropertyDescriptor.property("disabled", false);
private final PropertyDescriptor<Integer> max =
PropertyDescriptor.property("max", 100);
private final PropertyDescriptor<Double> step =
PropertyDescriptor.property("step", 1.0);
La sérialisation est automatique pour les primitifs, leurs équivalents boîte, et String. Pour les types complexes, la valeur est sérialisée en JSON avant d'être assignée à la propriété côté client.
Validation des valeurs
Validez les valeurs dans le setter avant d'appeler set(). Le setter est le point d'application naturel car chaque mutation passe par lui.
private final PropertyDescriptor<Integer> max =
PropertyDescriptor.property("max", 100);
public Slider setMax(int value) {
if (value < 0) {
throw new IllegalArgumentException("max doit être non-négatif");
}
set(max, value);
return this;
}
Pour les références nullables, utilisez Objects.requireNonNull() afin que l'échec surgisse à la frontière plutôt que plus tard dans le pipeline de rendu.
public Card setHeading(String value) {
Objects.requireNonNull(value, "heading ne peut pas être null");
set(heading, value);
return this;
}
Évitez de valider dans get(). Les lectures doivent rester peu coûteuses et cohérentes.
Propriétés de type enum
La plupart des web components s'attendent à des valeurs de chaînes en minuscules ou en kebab-case pour les propriétés de type énumération (theme="primary", expanse="xs"). webforJ utilise Gson pour sérialiser les énums, mais la représentation par défaut de Gson est le nom constant en majuscules. Annotez chaque constant avec @SerializedName afin que la valeur sérialisée corresponde à ce que le web component attend.
import com.google.gson.annotations.SerializedName;
public enum Variant {
@SerializedName("primary")
PRIMARY,
@SerializedName("secondary")
SECONDARY,
@SerializedName("danger")
DANGER
}
Déclarez le descripteur avec le type enum et utilisez l'énumération directement dans le setter et le getter.
private final PropertyDescriptor<Variant> variant =
PropertyDescriptor.property("variant", Variant.PRIMARY);
public MyButton setVariant(Variant value) {
set(variant, value);
return this;
}
public Variant getVariant() {
return get(variant);
}
C'est le même modèle que les composants intégrés de webforJ utilisent pour Theme, Expanse, et des énumérations similaires. L'API publique Java reste type-sûre, et la valeur que le web component reçoit est la chaîne provenant de @SerializedName.
Test des propriétés
PropertyDescriptorTester valide que chaque PropertyDescriptor dans un composant est câblé correctement. Il scanne la classe à la recherche des champs descripteurs, appelle chaque setter avec la valeur par défaut et compare le résultat avec ce que retourne le getter. Le testeur détecte les erreurs d'intégration avant qu'elles n'atteignent une application en cours d'exécution : un setter qui écrit sur le mauvais descripteur, un getter qui lit une propriété différente, une valeur par défaut qui ne fait pas un aller-retour, ou un accesseur manquant pour un descripteur déclaré.
Un test de base pour un composant ressemble à ceci :
import com.webforj.component.element.PropertyDescriptorTester;
import org.junit.jupiter.api.Test;
class CardTest {
@Test
void validateProperties() {
Card component = new Card();
PropertyDescriptorTester.run(Card.class, component);
}
}
Exclusion des propriétés
Certains descripteurs ne suivent pas les conventions standard des accesseurs et mutateurs, ou ils dépendent d'un état externe que le test ne peut pas satisfaire. Annotez-les avec @PropertyExclude pour les ignorer.
@PropertyExclude
private final PropertyDescriptor<String> internal =
PropertyDescriptor.property("internal", "");
Noms d'accesseurs et de mutateurs personnalisés
Si un descripteur utilise des noms d'accesseurs non standards, déclarez-les avec @PropertyMethods.
@PropertyMethods(getter = "retrieveValue", setter = "updateValue")
private final PropertyDescriptor<String> custom =
PropertyDescriptor.property("custom", "default");
Le paramètre target accepte une classe lorsque les accesseurs se trouvent ailleurs que dans le composant lui-même.
Pour plus de détails sur la surface de test, voir PropertyDescriptorTester.
Interfaces de préoccupation
Les interfaces de préoccupation donnent à une sous-classe d'ElementComposite des capacités sans écrire l'implémentation vous-même. Les interfaces transmettent les appels à l'élément sous-jacent. Implémentez celles que le composant doit supporter, paramétrées avec le type de la sous-classe pour que le chaînage renvoie le composant :
@NodeName("my-badge")
public class MyBadge extends ElementComposite
implements HasText<MyBadge>, HasClassName<MyBadge>, HasStyle<MyBadge> {
// Pas d'implémentation nécessaire.
}
MyBadge badge = new MyBadge()
.setText("Nouvelle")
.addClassName("highlight")
.setStyle("color", "var(--dwc-color-primary)");
Les trois interfaces ci-dessus couvrent tout ce dont MyBadge a besoin sans aucun corps de méthode dans la classe. HasText expose setText() et écrit dans le contenu texte de l'élément. HasClassName expose addClassName(), ce qui permet au badge d'être ciblé depuis le CSS. HasStyle expose setStyle() pour les styles en ligne.
Pour l'ensemble complet d'interfaces disponibles et ce que chacune fournit, voir Interfaces de préoccupation dans l'article Comprendre les Composants. Si le transfert par défaut ne correspond pas à ce que l'élément enveloppé expose, remplacez la méthode dans la sous-classe.
Événements
Enregistrement d'événements
Les web components déclenchent des événements DOM lorsque quelque chose se passe dans le navigateur. Pour réagir depuis Java, écoutez ces événements avec addEventListener(). L'ensemble des événements qu'un composant déclenche varie, alors consultez la documentation du composant pour les noms et charges utiles disponibles.
ElementComposite prend en charge les débounce, throttling, filtrage, et données d'événements personnalisées sur les écouteurs enregistrés.
Enregistrez des écouteurs d'événements en utilisant la méthode addEventListener() :
// Exemple : ajout d'un écouteur d'événements de clic
addEventListener(ElementClickEvent.class, event -> {
// Gérer l'événement de clic
});
ElementComposite n'accepte que les classes d'événements annotées avec @EventName, contrairement à Element, qui accepte n'importe quel nom d'événement sous forme de chaîne.
Classes d'événements intégrées
ElementClickEvent est la seule classe d'événements intégrée que ElementComposite fournit. Elle fait remonter les événements de clic de souris sur l'élément sous-jacent avec des accesseurs typés pour les coordonnées (getClientX(), getClientY()), les informations sur le bouton (getButton()), et les touches modificateurs (isCtrlKey(), isShiftKey(), etc.).
Pour exposer la gestion des clics sur l'API publique d'une sous-classe, implémentez l'interface de préoccupation HasElementClickListener<T>. Elle fournit des méthodes par défaut onClick() et addClickListener() qui délèguent à la primitive protégée addEventListener().
@NodeName("my-badge")
public class MyBadge extends ElementComposite
implements HasElementClickListener<MyBadge> {
// onClick() et addClickListener() sont maintenant disponibles sur MyBadge
}
new MyBadge().onClick(event -> {
if (event.isShiftKey()) {
// ...
}
});
Pour tout autre événement que le web component sous-jacent déclenche, définissez une classe d'événement personnalisée. Voir Classes d'événements personnalisées.
Charges utiles des événements
Les événements transportent des données du client vers votre code Java. Accédez à ces données via getData() pour les données d'événement brutes ou utilisez des méthodes typées lorsque disponibles sur les classes d'événements intégrées. Voir le guide des événements pour plus d'informations sur la gestion efficace des charges utiles.
Classes d'événements personnalisées
Définissez des classes d'événements personnalisées avec @EventName et @EventOptions pour capturer des données côté client dans un événement Java typé. Utilisez cela lorsque le gestionnaire Java a besoin de valeurs venant du navigateur.
@EventName lie la classe Java à l'événement que le composant déclenche dans le navigateur, donc une classe annotée @EventName("change") se déclenche chaque fois que l'élément sous-jacent émet change. @EventOptions contrôle ce qui voyage avec cet événement. Chaque @EventData à l'intérieur associe une clé à une expression JavaScript évaluée par rapport à l'événement DOM. Le résultat est disponible dans la classe d'événements Java via getData().get(key).
Le formulaire d'évaluation de produit ci-dessous utilise ce modèle avec wa-rating. Le ChangeEvent personnalisé porte la valeur de notation sous forme de double typé, et l'écouteur l'utilise pour activer le bouton de soumission :
Afficher le code
- RatingView.java
Options d'événements
ElementEventOptions configure la charge utile de l'événement, le timing de debouncing ou de throttling, les expressions de filtrage, et le code d'exécution préalable. L'extrait ci-dessous montre les options :
ElementEventOptions options = new ElementEventOptions()
// Collecter des données personnalisées depuis le client
.addData("query", "component.value")
.addData("timestamp", "Date.now()")
.addData("isValid", "component.checkValidity()")
// Exécuter JavaScript avant que l'événement ne se déclenche
.setCode("component.classList.add('processing');")
// Ne se déclencher que si les conditions sont remplies
.setFilter("component.value.length >= 2")
// Retarder l'exécution jusqu'à ce que l'utilisateur arrête de taper (300ms)
.setDebounce(300, DebouncePhase.TRAILING);
// Appliquer ces options lors de l'enregistrement d'un écouteur pour une classe d'événement personnalisée
// (voir la section Classes d'événements personnalisées ci-dessus pour comment en définir une) :
addEventListener(InputEvent.class, this::handleSearch, options);
ElementComposite expose uniquement la forme basée sur la classe addEventListener(Class, listener, options). Utilisez-la avec une classe d'événement annotée avec @EventName. Pour s'inscrire directement contre un nom d'événement sous forme de chaîne, appelez getElement().addEventListener("input", listener, options).
Contrôle de performance
Debouncing retarde l'exécution jusqu'à ce que l'activité s'arrête :
options.setDebounce(300, DebouncePhase.TRAILING); // Attendre 300ms après le dernier événement
Phases de débounce disponibles :
LEADING: Déclencher immédiatement, puis attendreTRAILING: Attendre une période de silence, puis déclencher (par défaut)BOTH: Déclencher immédiatement et après une période de silence
Throttling limite la fréquence d'exécution :
options.setThrottle(100); // Déclencher au maximum une fois par 100ms
Interagir avec les slots
Les slots sont des espaces réservés à l'intérieur d'un web component que les utilisateurs remplissent de contenu. Le web component déclare ses slots dans son modèle avec <slot> ou <slot name="...">, et le wrapper expose des méthodes qui ajoutent des composants Java à ces slots.
Pour ajouter du contenu aux slots, étendez ElementCompositeContainer au lieu de ElementComposite. Le conteneur porte la même machinerie de propriétés et d'attributs plus les méthodes nécessaires pour ajouter des enfants. Les enfants ajoutés via add() vont dans le slot par défaut. Les enfants ajoutés via getElement().add(slotName, components) vont dans le slot nommé.
@NodeName("my-dialog")
public class Dialog extends ElementCompositeContainer {
private final PropertyDescriptor<String> heading =
PropertyDescriptor.property("heading", "");
public Dialog setHeading(String value) {
set(heading, value);
return this;
}
public Dialog addToFooter(Component... components) {
getElement().add("footer", components);
return this;
}
}
La démo ci-dessous montre deux cartes de prix construites avec wa-card, peuplant les slots header, par défaut, et footer depuis Java :
Afficher le code
- WebAwesomeCardView.java
Inspecter le contenu des slots
L'Element sous-jacent (accessible via getElement()) fournit des méthodes pour lire ce qui est actuellement attribué aux slots :
findComponentSlot(): cherche tous les slots pour un composant spécifique et retourne le nom du slot le contenant, ou une chaîne vide si le composant n'est dans aucun slot.getComponentsInSlot(): retourne la liste des composants attribués à un slot donné. Prend éventuellement un type de classe pour filtrer les résultats.getFirstComponentInSlot(): retourne le premier composant attribué à un slot. Prend éventuellement un type de classe pour filtrer.
Keep learning
Vidéo · YouTubeBuild Modern Buttons in Java with webforJ
ArticleWeb components in Java: wrap any custom element with webforJ
ArticleBuilding an Interactive SVG Viewer with webforJ
ArticleSpring Security role-based UI access: why protecting routes beats hiding elements
ArticleA Java framework like React: components, state, routing
ArticleThe Demo That Taught Me webforJ Events
ArticleSimple Java web development: one language, no frontend stack
ArticleThe best Java UI framework for enterprise business apps in 2026
ArticleYour Spring Boot app doesn't need a REST API for the UI