Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,19 @@ Available in Vaadin 25+, this feature allows you to verify add-on behavior acros

<img width="91" height="106" alt="image" src="https://github.com/user-attachments/assets/cce58a29-f779-477d-89b4-ee845a80b962" />

To enable this feature, the `DynamicTheme` must be initialized in the `AppShellConfigurator` of the application. Ensure that the legacy `@Theme` annotation and any Aura or Lumo `@StyleSheet` references are removed.
To enable this feature, annotate the `AppShellConfigurator` of the application with `@DefaultDynamicTheme`. Ensure that the legacy `@Theme` annotation and any Aura or Lumo `@StyleSheet` references are removed.

```java
@DefaultDynamicTheme(DynamicTheme.LUMO)
public class AppShellConfiguratorImpl implements AppShellConfigurator {

@Override
public void configurePage(AppShellSettings settings) {
if (DynamicTheme.isFeatureSupported()) {
DynamicTheme.LUMO.initialize(settings);
}
}

}
```

The annotation is read when the application starts, so the default theme also applies to sessions that did not load `index.html` (for example, after the server restarts while a page is open, or when `index.html` is served from a cache).

Initializing `DynamicTheme` from the `configurePage` method of the `AppShellConfigurator` (i.e. calling `DynamicTheme.LUMO.initialize(settings)`) is deprecated, because the default theme is only known after `index.html` has been served at least once. Replace that call by annotating the `AppShellConfigurator` with `@DefaultDynamicTheme`.

When targeting Vaadin 14-25 or 23-25, the `AppShellConfigurator` approach cannot be used due to framework and library limitations.
Comment thread
paodb marked this conversation as resolved.
To resolve this, you must create a configuration file `src/test/resources/META-INF/dynamic-theme.properties` with the following content:
```properties
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*-
* #%L
* Commons Demo
* %%
* Copyright (C) 2020 - 2026 Flowing Code
* %%
* 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.
* #L%
*/
package com.flowingcode.vaadin.addons.demo;

import com.vaadin.flow.component.page.AppShellConfigurator;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Enables dynamic theme switching with the given theme as the default.
* <p>
* This annotation must be placed on the {@link AppShellConfigurator} of the application. It
* replaces calling {@link DynamicTheme#initialize(com.vaadin.flow.server.AppShellSettings)
Comment thread
paodb marked this conversation as resolved.
* DynamicTheme.initialize} from {@link AppShellConfigurator#configurePage configurePage}. Since
* the annotation is read when the application starts, the default theme also applies to sessions
* that did not load {@code index.html} (for instance, after a server restart, or when
* {@code index.html} was served from a cache).
* </p>
* <p>
* Dynamic theme switching is only available with Vaadin 25+. With older versions this annotation
* has no effect, so an {@link AppShellConfigurator} that runs on several Vaadin versions can be
* annotated as well. The {@link AppShellConfigurator} must not be annotated with the legacy
* {@link com.vaadin.flow.theme.Theme @Theme} annotation.
* </p>
* <p>
* The annotation is ignored if it is placed on a class that is not the {@link AppShellConfigurator}
* of the application. It is inherited by subclasses of the annotated class.
* </p>
*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DefaultDynamicTheme {
Comment thread
paodb marked this conversation as resolved.

/**
* The default theme.
*
* @return the default theme
*/
DynamicTheme value();

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,51 @@

/**
* Checks if the dynamic theme feature has been initialized for the current session.
* <p>
* The feature is initialized if a theme has been set for the current session, or if a default
* theme has been registered for the application.
* </p>
*
* @return {@code true} if the feature is supported and initialized; {@code false} otherwise.
*/
public static boolean isFeatureInitialized() {
return isFeatureSupported()
&& VaadinSession.getCurrent().getAttribute(DynamicTheme.class) != null;
&& (getSessionTheme() != null
|| getDefault(VaadinService.getCurrent().getContext()) != null);
}

private static DynamicTheme getSessionTheme() {
return VaadinSession.getCurrent().getAttribute(DynamicTheme.class);
}

private static DynamicTheme getDefault(VaadinContext context) {
DefaultTheme defaultTheme = context.getAttribute(DefaultTheme.class);
return defaultTheme != null ? defaultTheme.theme : null;
}

// Registers this theme as the application-wide default, which is used for sessions that did not
// go through the initialization of index.html (e.g. after a server restart, or when index.html
// was served from a cache).
void setDefault(VaadinContext context) {
context.setAttribute(DefaultTheme.class, new DefaultTheme(this));
}

private void setDefaultIfAbsent(VaadinContext context) {
// getAttribute(type, supplier) stores the supplied value when the attribute is absent
context.getAttribute(DefaultTheme.class, () -> new DefaultTheme(this));
}

@RequiredArgsConstructor
private static final class DefaultTheme {
private final DynamicTheme theme;
}

private static void assertNotLegacyTheme() {
VaadinContext context = VaadinService.getCurrent().getContext();
Class<? extends AppShellConfigurator> appShellClass =
AppShellRegistry.getInstance(context).getShell();
assertNotLegacyTheme(AppShellRegistry.getInstance(context).getShell());
}

static void assertNotLegacyTheme(Class<? extends AppShellConfigurator> appShellClass) {
if (appShellClass != null && appShellClass.getAnnotation(Theme.class) != null) {
throw new IllegalStateException("App shell is configured with legacy @Theme annotation");
}
Expand All @@ -115,7 +148,8 @@
*/
public static DynamicTheme getCurrent() {
assertFeatureSupported();
return VaadinSession.getCurrent().getAttribute(DynamicTheme.class);
DynamicTheme theme = getSessionTheme();
return theme != null ? theme : getDefault(VaadinService.getCurrent().getContext());
}

/**
Expand All @@ -126,17 +160,27 @@
* as the session default. Subsequently, it injects the corresponding CSS stylesheet
* link into the document head.
* </p>
* <p>
* If no application-wide default has been registered, this instance is also registered
* as the default for sessions that did not go through this initialization.
* </p>
*
* @param settings the application shell settings to be modified
* @throws UnsupportedOperationException if the runtime Vaadin version is older than 25
* @throws IllegalStateException if the {@link AppShellConfigurator} is configured with the legacy
* {@link Theme} annotation
* @deprecated Annotate the {@link AppShellConfigurator} with {@link DefaultDynamicTheme} instead.
* This method is only invoked when {@code index.html} is generated, therefore the
* default theme is unknown to sessions that did not load {@code index.html} until it
* has been served at least once.
*/
@Deprecated(since = "5.5.0", forRemoval = true)
public void initialize(AppShellSettings settings) {

Check warning on line 178 in base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicTheme.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_CommonsDemo&issues=AaDuUSqpoWonBeEyRka-&open=AaDuUSqpoWonBeEyRka-&pullRequest=173
assertFeatureSupported();
assertNotLegacyTheme();

DynamicTheme theme = getCurrent();
setDefaultIfAbsent(VaadinService.getCurrent().getContext());
DynamicTheme theme = getSessionTheme();
if (theme == null) {
theme = this;
VaadinSession.getCurrent().setAttribute(DynamicTheme.class, theme);
Expand All @@ -162,6 +206,10 @@
* as the session default. Subsequently, it injects the corresponding CSS stylesheet
* link into the document head.
* </p>
* <p>
* If no application-wide default has been registered, this instance is also registered
* as the default for sessions that did not go through this initialization.
* </p>
*
* @param response the index HTML response to be modified
* @throws UnsupportedOperationException if the runtime Vaadin version is older than 25
Expand All @@ -172,7 +220,8 @@
assertFeatureSupported();
assertNotLegacyTheme();

DynamicTheme theme = getCurrent();
setDefaultIfAbsent(VaadinService.getCurrent().getContext());
DynamicTheme theme = getSessionTheme();
if (theme == null) {
theme = this;
VaadinSession.getCurrent().setAttribute(DynamicTheme.class, theme);
Expand All @@ -191,10 +240,14 @@
}

if (href != null) {
Element link = response.getDocument().createElement("link");
link.attr("rel", "stylesheet");
link.attr("href", href);
response.getDocument().head().appendChild(link);
// skip the link if it was already added through AppShellSettings
String selector = "link[rel=stylesheet][href=\"" + href + "\"]";
if (response.getDocument().selectFirst(selector) == null) {
Element link = response.getDocument().createElement("link");
link.attr("rel", "stylesheet");
link.attr("href", href);
response.getDocument().head().appendChild(link);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*/
package com.flowingcode.vaadin.addons.demo;

import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.server.AppShellRegistry;
import com.vaadin.flow.server.ServiceInitEvent;
import com.vaadin.flow.server.VaadinServiceInitListener;
import com.vaadin.flow.server.communication.IndexHtmlRequestListener;
Expand All @@ -34,10 +36,12 @@
/**
* Service initialization listener that automatically applies a dynamic theme.
* <p>
* If the dynamic theme feature is supported, this listener checks for the presence of a
* {@code /META-INF/dynamic-theme.properties} file. If found, it reads the {@code theme} property
* (e.g., {@code theme=LUMO}) and registers an {@link IndexHtmlRequestListener} to initialize the
* theme for all requests.
* If the dynamic theme feature is supported, this listener checks whether the
* {@link AppShellConfigurator} is annotated with {@link DefaultDynamicTheme}. Otherwise, it checks
* for the presence of a {@code /META-INF/dynamic-theme.properties} file, and if found, it reads the
* {@code theme} property (e.g., {@code theme=LUMO}). The theme is registered as the default theme
* of the application, and an {@link IndexHtmlRequestListener} is registered to initialize the theme
* for all requests.
* </p>
*/
@SuppressWarnings("serial")
Expand All @@ -48,17 +52,40 @@
private static final String PROPERTIES_PATH = "META-INF/dynamic-theme.properties";

@Override
public void serviceInit(ServiceInitEvent event) {

Check failure on line 55 in base/src/main/java/com/flowingcode/vaadin/addons/demo/DynamicThemeInitializer.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=FlowingCode_CommonsDemo&issues=AaDuUSn-oWonBeEyRka9&open=AaDuUSn-oWonBeEyRka9&pullRequest=173
if (DynamicTheme.isFeatureSupported()) {
Class<? extends AppShellConfigurator> appShellClass =
Comment thread
paodb marked this conversation as resolved.
AppShellRegistry.getInstance(event.getSource().getContext()).getShell();
if (appShellClass == null) {
logger.debug("No AppShellConfigurator is registered, @DefaultDynamicTheme is not applied");
}
DefaultDynamicTheme annotation = appShellClass != null
? appShellClass.getAnnotation(DefaultDynamicTheme.class)
: null;
if (annotation != null) {
DynamicTheme.assertNotLegacyTheme(appShellClass);
DynamicTheme theme = annotation.value();
logger.info("Applying dynamic theme '{}' from {}", theme, appShellClass.getName());
theme.setDefault(event.getSource().getContext());
Comment thread
paodb marked this conversation as resolved.
event.addIndexHtmlRequestListener(theme::initialize);
return;
}

try {
Enumeration<URL> resources = getClass().getClassLoader().getResources(PROPERTIES_PATH);
boolean hasDefault = false;
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
String source = getSourceName(url);
readTheme(url).ifPresent(theme -> {
logger.info("Applying dynamic theme '{}' from {}", theme, source);
event.addIndexHtmlRequestListener(theme::initialize);
});
Optional<DynamicTheme> theme = readTheme(url);
if (theme.isPresent()) {
logger.info("Applying dynamic theme '{}' from {}", theme.get(), getSourceName(url));
// the first listener initializes the session, so its theme is the default
if (!hasDefault) {
theme.get().setDefault(event.getSource().getContext());
hasDefault = true;
}
event.addIndexHtmlRequestListener(theme.get()::initialize);
}
}
} catch (IOException e) {
throw new RuntimeException("Error reading dynamic-theme.properties", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,8 @@
package com.flowingcode.vaadin.addons.demo;

import com.vaadin.flow.component.page.AppShellConfigurator;
import com.vaadin.flow.server.AppShellSettings;

@DefaultDynamicTheme(DynamicTheme.LUMO)
public class AppShellConfiguratorImpl implements AppShellConfigurator {

@Override
public void configurePage(AppShellSettings settings) {
if (DynamicTheme.isFeatureSupported()) {
DynamicTheme.LUMO.initialize(settings);
}
}

}
Loading