Skip to content

Commit

Permalink
Feat: enable having a parent theme (#9648) (#10226)
Browse files Browse the repository at this point in the history
It is now possible to extend a parent theme.
This means adding to theme.json the key "parent" with
the value of the target theme and then this will
be loaded before the application's own theme.

Fixes #9587
  • Loading branch information
caalador committed Mar 9, 2021
1 parent c10f170 commit 031da61
Show file tree
Hide file tree
Showing 20 changed files with 475 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
const fs = require('fs');
const path = require('path');
const generateThemeFile = require('./theme-generator');
const { copyStaticAssets } = require('./theme-copy');
const {copyStaticAssets} = require('./theme-copy');

let logger;
let executionOptions;

// matches theme folder name in 'themes/my-theme/my-theme.js'
const nameRegex = /themes\/(.*)\/\1.generated.js/g;
Expand All @@ -32,6 +33,8 @@ const nameRegex = /themes\/(.*)\/\1.generated.js/g;
* themeResourceFolder - theme folder where flow copies local and jar resource frontend files
* themeProjectFolders - array of possible locations for theme folders inside the project
* projectStaticAssetsOutputFolder - path to where static assets should be put
*
* @throws Error in constructor if required option is not received
*/
class ApplicationThemePlugin {
constructor(options) {
Expand Down Expand Up @@ -60,32 +63,9 @@ class ApplicationThemePlugin {
if (!themeName) {
throw new Error("Couldn't parse theme name from '" + generatedThemeFile + "'.");
}
executionOptions = this.options;
findThemeFolderAndHandleTheme(themeName);

let themeFound = false;
for (let i = 0; i<this.options.themeProjectFolders.length; i++) {
const themeProjectFolder = this.options.themeProjectFolders[i];
if (fs.existsSync(themeProjectFolder)) {
logger.info("Searching themes folder ", themeProjectFolder, " for theme ", themeName);
const handled = handleThemes(themeName, themeProjectFolder, this.options.projectStaticAssetsOutputFolder);
if (handled) {
if(themeFound) {
throw new Error("Found theme files in '" + themeProjectFolder + "' and '"
+ themeFound + "'. Theme should only be available in one folder");
}
logger.info("Found theme files from '", themeProjectFolder, "'");
themeFound = themeProjectFolder;
}
}
}

if (fs.existsSync(this.options.themeResourceFolder)) {
if (themeFound && fs.existsSync(path.resolve(this.options.themeResourceFolder, themeName))) {
throw new Error("Theme '" + themeName + "'should not exist inside a jar and in the project at the same time\n" +
"Extending another theme is possible by adding { \"parent\": \"my-parent-theme\" } entry to the theme.json file inside your theme folder.");
}
logger.debug("Searching theme jar resource folder ", this.options.themeResourceFolder, " for theme ", themeName);
handleThemes(themeName, this.options.themeResourceFolder, this.options.projectStaticAssetsOutputFolder);
}
} else {
logger.debug("Skipping Vaadin application theme handling.");
logger.trace("Most likely no @Theme annotation for application or only themeClass used.");
Expand All @@ -96,13 +76,56 @@ class ApplicationThemePlugin {

module.exports = ApplicationThemePlugin;

/**
* Search for the given theme in the project and resource folders.
*
* @param {string} name of theme to find
*
* @return true or false for if theme was found
*/
function findThemeFolderAndHandleTheme(themeName) {

let themeFound = false;
for (let i = 0; i < executionOptions.themeProjectFolders.length; i++) {
const themeProjectFolder = executionOptions.themeProjectFolders[i];
if (fs.existsSync(themeProjectFolder)) {
logger.info("Searching themes folder", themeProjectFolder, "for theme", themeName);
const handled = handleThemes(themeName, themeProjectFolder, executionOptions.projectStaticAssetsOutputFolder);
if (handled) {
if (themeFound) {
throw new Error("Found theme files in '" + themeProjectFolder + "' and '"
+ themeFound + "'. Theme should only be available in one folder");
}
logger.info("Found theme files from '" + themeProjectFolder + "'");
themeFound = themeProjectFolder;
}
}
}

if (fs.existsSync(executionOptions.themeResourceFolder)) {
if (themeFound && fs.existsSync(path.resolve(executionOptions.themeResourceFolder, themeName))) {
throw new Error("Theme '" + themeName + "'should not exist inside a jar and in the project at the same time\n" +
"Extending another theme is possible by adding { \"parent\": \"my-parent-theme\" } entry to the theme.json file inside your theme folder.");
}
logger.debug("Searching theme jar resource folder ", executionOptions.themeResourceFolder, " for theme ", themeName);
handleThemes(themeName, executionOptions.themeResourceFolder, executionOptions.projectStaticAssetsOutputFolder);
themeFound = true;
}
return themeFound;
}

/**
* Copies static resources for theme and generates/writes the [theme-name].js for webpack to handle.
*
* Note! If a parent theme is defined it will also be handled here so that the parent theme generated file is
* generated in advance of the theme generated file.
*
* @param {string} themeName name of theme to handle
* @param {string} themesFolder folder containing application theme folders
* @param {string} projectStaticAssetsOutputFolder folder to output files to
*
* @throws Error if parent theme defined, but can't locate parent theme
*
* @returns true if theme was found else false.
*/
function handleThemes(themeName, themesFolder, projectStaticAssetsOutputFolder) {
Expand All @@ -112,6 +135,15 @@ function handleThemes(themeName, themesFolder, projectStaticAssetsOutputFolder)

const themeProperties = getThemeProperties(themeFolder);

// If theme has parent handle parent theme immediately.
if (themeProperties.parent) {
const found = findThemeFolderAndHandleTheme(themeProperties.parent);
if (!found) {
throw new Error("Could not locate files for defined parent theme '" + themeProperties.parent + "'.\n" +
"Please verify that dependency is added or theme folder exists.")
}
}

copyStaticAssets(themeName, themeProperties, projectStaticAssetsOutputFolder, logger);

const themeFile = generateThemeFile(themeFolder, themeName, themeProperties);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,20 @@ function generateThemeFile(themeFolder, themeName, themeProperties) {

let themeFile = headerImport;

if(componentsFiles.length > 0){
themeFile += 'import { css, unsafeCSS, registerStyles } from \'@vaadin/vaadin-themable-mixin/register-styles\';';
if (componentsFiles.length > 0) {
themeFile += 'import { css, unsafeCSS, registerStyles } from \'@vaadin/vaadin-themable-mixin/register-styles\';\n';
}

if (themeProperties.parent) {
themeFile += `import {applyTheme as applyBaseTheme} from 'themes/${themeProperties.parent}/${themeProperties.parent}.generated.js';`;
}

themeFile += injectGlobalCssMethod;

const imports = [];
const globalCssCode = [];
const componentCssCode = [];

const parentTheme = themeProperties.parent ? 'applyBaseTheme(target);\n' : '';
globalFiles.forEach((global) => {
const filename = path.basename(global);
const variable = camelCase(filename);
Expand Down Expand Up @@ -149,6 +153,7 @@ window.Vaadin.Flow['${globalCssFlag}'] = window.Vaadin.Flow['${globalCssFlag}']
// Don't format as the generated file formatting will get wonky!
// If targets check that we only register the style parts once, checks exist for global css and component css
const themeFileApply = `export const applyTheme = (target) => {
${parentTheme}
const injectGlobal = (window.Vaadin.Flow['${globalCssFlag}'].length === 0) || (!window.Vaadin.Flow['${globalCssFlag}'].includes(target) && target !== document);
if (injectGlobal) {
${globalCssCode.join('')}
Expand All @@ -173,7 +178,7 @@ window.Vaadin.Flow['${globalCssFlag}'] = window.Vaadin.Flow['${globalCssFlag}']
* @returns {string} camelCased version
*/
function camelCase(str) {
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(word, index) {
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index === 0 ? word.toLowerCase() : word.toUpperCase();
}).replace(/\s+/g, '').replace(/\.|\-/g, '');
}
Expand Down
1 change: 1 addition & 0 deletions flow-tests/test-application-theme/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<modules>
<module>reusable-theme</module>
<module>test-theme-reusable</module>
<module>test-reusable-as-parent</module>
</modules>

</project>
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@

@font-face {
font-family: 'IBM Plex Mono';
font-style: normal;
font-weight: 400;
src: url("./font/IBMPlexMono-Regular.otf") format("opentype");

}

html, :host {
--lumo-body-text-color: #008000;
}

body {
background-image: url("./bg.jpg");
font-family: "IBM Plex Mono";
}

#butterfly {
background-image: url("./img/gobo.png");
width: 280px;
height: 240px;
}

#octopuss {
background-image: url('./img/viking.png');
width: 270px;
height: 220px;
}

Binary file not shown.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"parent": "reusable-theme"
}
100 changes: 100 additions & 0 deletions flow-tests/test-application-theme/test-reusable-as-parent/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>test-application-theme</artifactId>
<groupId>com.vaadin</groupId>
<version>2.6-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>flow-test-use-reusable-as-parent</artifactId>
<name>Test reusable application theme as parent</name>
<packaging>war</packaging>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>

<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>flow-test-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>flow-html-components-testbench</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-lumo-theme</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>reusable-theme</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>com.vaadin</groupId>
<artifactId>flow-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>prepare-frontend</goal>
<goal>build-frontend</goal>
</goals>
</execution>
</executions>
<configuration>
<productionMode>true</productionMode>
</configuration>
</plugin>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>local-run</id>
<activation>
<property>
<name>!test.use.hub</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>com.lazerycode.selenium</groupId>
<artifactId>driver-binary-downloader-maven-plugin</artifactId>
<version>${driver.binary.downloader.maven.plugin.version}</version>
<configuration>
<onlyGetDriversForHostOperatingSystem>true</onlyGetDriversForHostOperatingSystem>
<rootStandaloneServerDirectory>${project.rootdir}/driver</rootStandaloneServerDirectory>
<downloadedZipFileDirectory>${project.rootdir}/driver_zips</downloadedZipFileDirectory>
<customRepositoryMap>${project.rootdir}/drivers.xml</customRepositoryMap>
</configuration>
<executions>
<execution>
<phase>pre-integration-test</phase>
<goals>
<goal>selenium</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2000-2021 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.servlets;

import javax.servlet.annotation.WebServlet;

import com.vaadin.flow.server.VaadinServlet;

/**
* This is a temporary workaround until #5740 is fixed.
*
* @since 2.0
*/
@WebServlet("/*")
public class WorkaroundServlet extends VaadinServlet {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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.uitest.ui.theme;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;

/**
* Polymer version of vaadin text field for testing component theming.
*/
@JsModule("@vaadin/vaadin-text-field/vaadin-text-field.js")
@Tag("vaadin-text-field")
@NpmPackage(value="@vaadin/vaadin-text-field", version = "2.7.1")
public class MyPolymerField extends Component {

/**
* Set the component id.
*
* @param id
* value to set
* @return this component
*/
public Component withId(String id) {
setId(id);
return this;
}
}

0 comments on commit 031da61

Please sign in to comment.