Skip to content

Commit

Permalink
feat: Theme can be defined as string or class (#9349)
Browse files Browse the repository at this point in the history
Add a string definition for theme that matches the "application theme" in the theme folder inside /frontend/theme/, loading the css automatically from there. This is based on Lumo theme, always.
Change old class based theme to use themeClass for theme selection.

Fixes #9281
# Conflicts:
#	flow-component-demo-helpers/src/main/java/com/vaadin/flow/demo/DemoView.java
#	flow-server/src/main/java/com/vaadin/flow/component/webcomponent/WebComponentUI.java
#	flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeTasks.java
#	flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdateImports.java
#	flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdateWebpack.java
#	flow-server/src/main/java/com/vaadin/flow/theme/Theme.java
#	flow-server/src/main/resources/webpack.generated.js
#	flow-server/src/test/java/com/vaadin/flow/server/frontend/scanner/FrontendDependenciesTest.java
#	flow-server/src/test/java/com/vaadin/flow/server/startup/VaadinAppShellInitializerTest.java
#	flow-test-generic/src/main/java/com/vaadin/flow/testutil/ClassesSerializableTest.java
#	flow-tests/test-lumo-theme/pom.xml
#	flow-tests/test-lumo-theme/src/main/java/com/vaadin/flow/uitest/ui/lumo/ExplicitLumoTemplateView.java
#	flow-tests/test-material-theme/pom.xml
#	flow-tests/test-material-theme/src/main/java/com/vaadin/flow/uitest/ui/material/MaterialThemedTemplateView.java
#	flow-tests/test-misc/src/main/java/com/vaadin/flow/misc/ui/MiscelaneousView.java
#	flow-tests/test-themes/pom.xml
#	flow-tests/test-themes/src/test/java/com/vaadin/flow/uitest/ui/theme/NpmThemedComponentIT.java
  • Loading branch information
caalador committed Mar 1, 2021
1 parent b88b15b commit 6e8384e
Show file tree
Hide file tree
Showing 74 changed files with 1,068 additions and 272 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ flow-tests/**/pnpmfile.js
flow-tests/**/pnpm-lock.yaml
flow-tests/**/tsconfig.json
flow-tests/**/types.d.ts
flow-tests/test-themes/frontend/theme/app-theme/app-theme.js
package.json
package-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public static class TranslatedImports extends Component {
}

@Route
@Theme(value = Lumo.class, variant = Lumo.DARK)
@Theme(value =Lumo.class, variant = Lumo.DARK)
public static class MainView extends Component {
ButtonComponent buttonComponent;
IconComponent iconComponent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public FrontendWebComponentGenerator(ClassFinder finder) {
* outputDirectory}.
*
* @param outputDirectory
* target directory for the web component module files
* target directory for the web component module files
* @return generated files
* @throws java.lang.IllegalStateException
* if {@code finder} cannot locate required classes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,9 @@ private NodeTasks(Builder builder) {
builder.frontendDirectory, builder.tokenFile,
builder.tokenFileData, builder.enablePnpm,
builder.additionalFrontendModules));

commands.add(new TaskUpdateThemeImport(builder.npmFolder,
frontendDependencies.getThemeDefinition()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ static Map<String, String> getDefaultDevDependencies() {
defaults.put("compression-webpack-plugin", "4.0.1");
defaults.put("webpack-merge", "4.2.2");
defaults.put("raw-loader", "3.1.0");
defaults.put("css-loader", "4.2.1");

defaults.put("typescript", "4.0.3");
defaults.put("ts-loader", "8.0.12");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
import elemental.json.JsonArray;
import elemental.json.JsonObject;
import elemental.json.impl.JsonUtil;

import static com.vaadin.flow.server.frontend.FrontendUtils.IMPORTS_NAME;

/**
Expand Down Expand Up @@ -139,6 +138,13 @@ protected Collection<String> getThemeLines() {
Collection<String> lines = new ArrayList<>();
AbstractTheme theme = getTheme();
ThemeDefinition themeDef = getThemeDefinition();

if (themeDef != null && !"".equals(themeDef.getName())) {
// If we define a theme name we need to import theme/theme-generated.js
lines.add("import {applyTheme} from 'theme/theme-generated.js';");
lines.add("applyTheme(document);");
}

if (theme != null) {
if (!theme.getHeaderInlineContents().isEmpty()) {
lines.add(THEME_PREPARE);
Expand All @@ -147,9 +153,11 @@ protected Collection<String> getThemeLines() {
String.format(THEME_LINE_TPL, NEW_LINE_TRIM
.matcher(html).replaceAll(""))));
}
theme.getHtmlAttributes(themeDef.getVariant())
.forEach((key, value) -> addLines(lines,
String.format(THEME_VARIANT_TPL, key, value)));
if (themeDef != null) {
theme.getHtmlAttributes(themeDef.getVariant()).forEach(
(key, value) -> addLines(lines,
String.format(THEME_VARIANT_TPL, key, value)));
}
lines.add("");
}
return lines;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2000-2020 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.server.frontend;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;

import org.apache.commons.io.FileUtils;
import org.slf4j.LoggerFactory;

import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.theme.ThemeDefinition;

/**
* Task for generating the theme-generated.js file for importing application
* theme.
*
* @since
*/
public class TaskUpdateThemeImport implements FallibleCommand {

private File themeImportFile;
private ThemeDefinition theme;

TaskUpdateThemeImport(File npmFolder, ThemeDefinition theme) {
File generatedDir = new File(npmFolder, FrontendUtils.DEFAULT_GENERATED_DIR);
this.themeImportFile = new File(new File(generatedDir, "theme"),
"theme-generated.js");
this.theme = theme;
}

@Override
public void execute() throws ExecutionFailedException {
if (theme == null || theme.getName().isEmpty()) {
return;
}
if (!themeImportFile.getParentFile().mkdirs()) {
LoggerFactory.getLogger(getClass()).debug(
"Didn't create folders as they probably already exist. "
+ "If there is a problem check access rights for folder {}",
themeImportFile.getParentFile().getAbsolutePath());
}

try {
FileUtils.write(themeImportFile,
"import {applyTheme as _applyTheme} from 'theme/" + theme
.getName() + "/" + theme.getName()
+ ".js';\nexport const applyTheme = _applyTheme;\n",
StandardCharsets.UTF_8);
} catch (IOException e) {
System.out.println("Throwing exception " + e.getMessage());
throw new ExecutionFailedException(
"Unable to write theme import file", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_GENERATED_DIR;
import static com.vaadin.flow.server.frontend.FrontendUtils.WEBPACK_CONFIG;
import static com.vaadin.flow.server.frontend.FrontendUtils.WEBPACK_GENERATED;

Expand All @@ -46,6 +47,8 @@ public class TaskUpdateWebpack implements FallibleCommand {
private final Path flowImportsFilePath;
private final Path webpackConfigPath;
private final Path frontendDirectory;
private final Path flowResourcesFolder;
private final Path resourceFolder;

/**
* Create an instance of the updater given all configurable parameters.
Expand Down Expand Up @@ -74,6 +77,10 @@ public class TaskUpdateWebpack implements FallibleCommand {
this.webpackOutputPath = webpackOutputDirectory.toPath();
this.flowImportsFilePath = generatedFlowImports.toPath();
this.webpackConfigPath = webpackConfigFolder.toPath();
this.flowResourcesFolder = new File(webpackConfigFolder,
DEFAULT_GENERATED_DIR).toPath();
this.resourceFolder = new File(webpackOutputDirectory.getParentFile(),
"resources").toPath();
}

@Override
Expand Down Expand Up @@ -126,18 +133,26 @@ private void createWebpackConfig() throws IOException {
+ getEscapedRelativeWebpackPath(webpackOutputPath) + "');";
String mainLine = "const fileNameOfTheFlowGeneratedMainEntryPoint = require('path').resolve(__dirname, '"
+ getEscapedRelativeWebpackPath(flowImportsFilePath) + "');";

String devmodeGizmoJSLine = "const devmodeGizmoJS = '" + FrontendUtils.DEVMODE_GIZMO_MODULE + "'";

String frontendFolder =
"const flowFrontendFolder = require('path').resolve(__dirname, '" + getEscapedRelativeWebpackPath(
flowResourcesFolder) + "');";
String assetsResourceFolder =
"const projectStaticAssetsOutputFolder = require('path').resolve(__dirname, '"
+ getEscapedRelativeWebpackPath(resourceFolder) + "');";


for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i).trim();
if (lines.get(i).startsWith(
"const fileNameOfTheFlowGeneratedMainEntryPoint")
&& !line.equals(mainLine)) {
"const fileNameOfTheFlowGeneratedMainEntryPoint")
&& !line.equals(mainLine)) {
lines.set(i, mainLine);
}
if (lines.get(i)
.startsWith("const mavenOutputFolderForFlowBundledFiles")
&& !line.equals(outputLine)) {
.startsWith("const mavenOutputFolderForFlowBundledFiles")
&& !line.equals(outputLine)) {
lines.set(i, outputLine);
}
if (lines.get(i).startsWith("const frontendFolder")) {
Expand All @@ -147,6 +162,12 @@ private void createWebpackConfig() throws IOException {
if (lines.get(i).startsWith("const devmodeGizmoJS")) {
lines.set(i, devmodeGizmoJSLine);
}
if (lines.get(i).startsWith("const flowFrontendFolder")) {
lines.set(i, frontendFolder);
}
if (lines.get(i).startsWith("const projectStaticAssetsOutputFolder")) {
lines.set(i, assetsResourceFolder);
}
}

FileUtils.writeLines(generatedFile, lines);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ final class FrontendClassVisitor extends ClassVisitor {
private static final String VARIANT = "variant";
private static final String LAYOUT = "layout";
static final String VALUE = "value";
static final String THEME_FOLDER = "themeFolder";
static final String VERSION = "version";
static final String ID = "id";
static final String INCLUDE = "include";
Expand Down Expand Up @@ -159,9 +160,11 @@ public void visit(String name, Object value) {
themeRouteVisitor = new RepeatedAnnotationVisitor() {
@Override
public void visit(String name, Object value) {
if (VALUE.equals(name)) {
endPoint.theme.name = ((Type) value).getClassName();
children.add(endPoint.theme.name);
if (THEME_FOLDER.equals(name)) {
endPoint.theme.themeName = (String)value;
} else if (VALUE.equals(name)) {
endPoint.theme.themeClass = ((Type) value).getClassName();
children.add(endPoint.theme.themeClass);
} else if (VARIANT.equals(name)) {
endPoint.theme.variant = value.toString();
}
Expand All @@ -171,7 +174,9 @@ public void visit(String name, Object value) {
themeLayoutVisitor = new RepeatedAnnotationVisitor() {
@Override
public void visit(String name, Object value) {
if (VALUE.equals(name) && endPoint.theme.name == null) {
if (THEME_FOLDER.equals(name)) {
themeRouteVisitor.visit(name, value);
} else if (VALUE.equals(name) && endPoint.theme.themeClass == null) {
themeRouteVisitor.visit(name, value);
} else if (VARIANT.equals(name)
&& endPoint.theme.variant.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,26 +267,28 @@ private void computeApplicationTheme() throws ClassNotFoundException,
visitClass(endPoint.getLayout(), endPoint, false);
}
if (endPoint.getTheme() != null) {
visitClass(endPoint.getTheme().getName(), endPoint, true);
visitClass(endPoint.getTheme().getThemeClass(), endPoint, true);
}
}

Set<ThemeData> themes = endPoints.values().stream()
// consider only endPoints with theme information
.filter(data -> data.getTheme().getName() != null
.filter(data -> data.getTheme().getThemeClass() != null ||
(data.getTheme().getThemeName() != null && !data.getTheme().getThemeName().isEmpty())
|| data.getTheme().isNotheme())
.map(EndPointData::getTheme)
// Remove duplicates by returning a set
.collect(Collectors.toSet());

if (themes.size() > 1) {
String names = endPoints.values().stream()
.filter(data -> data.getTheme().getName() != null
.filter(data -> data.getTheme().getThemeClass() != null ||
data.getTheme().getThemeName() != null
|| data.getTheme().isNotheme())
.map(data -> "found '"
+ (data.getTheme().isNotheme()
? NoTheme.class.getName()
: data.getTheme().getName())
: data.getTheme().getThemeName())
+ "' in '" + data.getName() + "'")
.collect(Collectors.joining("\n "));
throw new IllegalStateException(
Expand All @@ -296,21 +298,27 @@ private void computeApplicationTheme() throws ClassNotFoundException,

Class<? extends AbstractTheme> theme = null;
String variant = "";
String themeName = "";
if (themes.isEmpty()) {
theme = getDefaultTheme();
} else {
// we have a proper theme or no-theme for the app
ThemeData themeData = themes.iterator().next();
if (!themeData.isNotheme()) {
variant = themeData.getVariant();
theme = getFinder().loadClass(themeData.getName());
String themeClass = themeData.getThemeClass();
if (themeClass == null) {
themeClass = LUMO;
}
theme = getFinder().loadClass(themeClass);
themeName = themeData.getThemeName();
}

}

// theme could be null when lumo is not found or when a NoTheme found
if (theme != null) {
themeDefinition = new ThemeDefinition(theme, variant);
themeDefinition = new ThemeDefinition(theme, variant, themeName);
themeInstance = new ThemeWrapper(theme);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ private void discoverTheme() {
ThemeData data = verifyTheme();

if (data == null) {
setupTheme(getLumoTheme(), "");
setupTheme(getLumoTheme(), "", "");
return;
}

Expand All @@ -274,18 +274,19 @@ private void discoverTheme() {

try {
Class<? extends AbstractTheme> theme = getFinder()
.loadClass(data.name);
setupTheme(theme, data.variant);
.loadClass(data.getThemeClass());
setupTheme(theme, data.getVariant(), data.getThemeName());
} catch (ClassNotFoundException exception) {
throw new IllegalStateException(
"Could not load theme class " + data.name, exception);
"Could not load theme class " + data.getThemeClass(),
exception);
}
}

private void setupTheme(Class<? extends AbstractTheme> theme,
String variant) {
String variant, String name) {
if (theme != null) {
themeDefinition = new ThemeDefinition(theme, variant);
themeDefinition = new ThemeDefinition(theme, variant, name);
try {
themeInstance = new ThemeWrapper(theme);
} catch (InstantiationException | IllegalAccessException e) {
Expand All @@ -308,7 +309,8 @@ private ThemeData verifyTheme() {
.map(theme -> new ThemeData(
((Class<?>) invokeAnnotationMethod(theme, VALUE))
.getName(),
invokeAnnotationMethodAsString(theme, "variant")))
invokeAnnotationMethodAsString(theme, "variant"),
invokeAnnotationMethodAsString(theme, "themeFolder")))
.collect(Collectors.toSet());

Class<? extends Annotation> loadedNoThemeAnnotation = getFinder()
Expand Down Expand Up @@ -342,9 +344,10 @@ private ThemeData verifyTheme() {
}

private String getThemesList(Collection<ThemeData> themes) {
return themes
.stream().map(theme -> "name = '" + theme.getName()
+ "' and variant = '" + theme.getVariant() + "'")
return themes.stream()
.map(theme -> "themeClass = '" + theme.getThemeClass()
+ "' and variant = '" + theme.getVariant()
+ "' and name = '" + theme.getThemeName() + "'")
.collect(Collectors.joining(", "));
}

Expand Down

0 comments on commit 6e8384e

Please sign in to comment.