Skip to content

Instantly share code, notes, and snippets.

@TatuLund
Created July 8, 2024 12:09
Show Gist options
  • Select an option

  • Save TatuLund/2a37c5b85677e6df0c9cdd52ee5c3980 to your computer and use it in GitHub Desktop.

Select an option

Save TatuLund/2a37c5b85677e6df0c9cdd52ee5c3980 to your computer and use it in GitHub Desktop.
This is a DemoView and helper class for Spring Boot based (WAR packaging required) projects to show demo code snippets.
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>flow-component-demo-helpers</artifactId>
<version>9.0.13</version>
</dependency>
package com.example.application.views;
/*
* 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.
*/
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasComponents;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.HasTheme;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.dependency.StyleSheet;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.demo.Card;
import com.vaadin.flow.demo.DemoNavigationBar;
import com.vaadin.flow.demo.SourceCodeExample;
import com.vaadin.flow.demo.SourceContent;
import com.vaadin.flow.demo.WhenDefinedManager;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.OptionalParameter;
import com.vaadin.flow.router.Route;
/**
* Base class for all the Views that demo some component. Modified version of
* DemoView for Spring.
*/
@Tag(Tag.DIV)
@StyleSheet("frontend/src/css/demo.css")
@StyleSheet("frontend/src/css/prism.css")
public abstract class SpringDemoView extends Component
implements HasComponents, HasUrlParameter<String>, HasStyle {
static final String VARIANT_TOGGLE_BUTTONS_DIV_ID = "variantToggleButtonsDiv";
static final String COMPONENT_WITH_VARIANTS_ID = "componentWithVariantsDemo";
private final DemoNavigationBar navBar = new DemoNavigationBar();
private final Div container = new Div();
private final Map<String, Div> tabComponents = new HashMap<>();
private final Map<String, List<SourceCodeExample>> sourceCodeExamples = new HashMap<>();
protected void doInit() {
Route annotation = getClass().getAnnotation(Route.class);
if (annotation == null) {
throw new IllegalStateException(
getClass().getName() + " should be annotated with @"
+ Route.class.getName() + " to be a valid view");
}
addClassName("demo-view");
navBar.addClassName("demo-nav");
add(navBar);
add(container);
populateSources();
initView();
}
@Override
protected void onAttach(AttachEvent attachEvent) {
if (tabComponents.size() <= 1) {
remove(navBar);
}
}
/**
* Builds the content of the view.
*/
protected abstract void initView();
/**
* When called the view should populate the given SourceContainer with
* sample source code to be shown.
*/
public void populateSources() {
SpringSourceContentResolver.getSourceCodeExamplesForClass(getClass())
.forEach(this::putSourceCode);
}
private void putSourceCode(SourceCodeExample example) {
String heading = example.getHeading();
List<SourceCodeExample> list = sourceCodeExamples
.computeIfAbsent(heading, key -> new ArrayList<>());
list.add(example);
}
/**
* Creates and adds a new component card to the "Basic usage" tab in the
* view. It automatically adds any source code examples with the same
* heading to the bottom of the card.
*
* @param heading
* the header text of the card, that is added to the layout. If
* <code>null</code> or empty, the header is not added
*
* @param components
* components to add on creation. If <code>null</code> or empty,
* the card is created without the components inside
* @return created component container card
* @see #addCard(String, String, Component...)
*/
public Card addCard(String heading, Component... components) {
return addCard("Basic usage", "", heading, components);
}
/**
* Creates and adds a new component card to a specific tab in the view. It
* automatically adds any source code examples with the same heading to the
* bottom of the card.
* <p>
* The href of the tab is defined based on the tab name. For example, a tab
* named "Advanced usage" has the "advanced-tab" as href (all in lower case
* and with "-" in place of spaces and special characters).
*
* @param tabName
* the name of the tab that will contain the demo, not
* <code>null</code>
* @param heading
* the header text of the card, that is added to the layout. If
* <code>null</code> or empty, the header is not added
* @param components
* components to add on creation. If <code>null</code> or empty,
* the card is created without the components inside
* @return created component container card
*/
public Card addCard(String tabName, String heading,
Component... components) {
String tabUrl = tabName.toLowerCase().replaceAll("[\\W]", "-");
return addCard(tabName, tabUrl, heading, components);
}
private Card addCard(String tabName, String tabUrl, String heading,
Component... components) {
Div tab = tabComponents.computeIfAbsent(tabUrl, url -> {
navBar.addLink(tabName, getTabUrl(tabUrl));
return new Div();
});
if (heading != null && !heading.isEmpty()) {
tab.add(new H3(heading));
}
Card card = new Card();
card.getElement().getNode().runWhenAttached(ui -> {
WhenDefinedManager.get(ui).whenDefined(components, () -> {
if (components != null && components.length > 0) {
card.add(components);
}
List<SourceCodeExample> list = sourceCodeExamples.get(heading);
if (list != null) {
list.stream().map(this::createSourceContent)
.forEach(card::add);
}
});
});
tab.add(card);
return card;
}
private String getTabUrl(String relativeHref) {
String href = relativeHref == null || relativeHref.isEmpty() ? ""
: "/" + relativeHref;
return getClass().getAnnotation(Route.class).value() + href;
}
private SourceContent createSourceContent(
SourceCodeExample sourceCodeExample) {
SourceContent content = new SourceContent();
String sourceString = sourceCodeExample.getSourceCode();
switch (sourceCodeExample.getSourceType()) {
case CSS:
content.addCss(sourceString);
break;
case JAVA:
content.addCode(sourceString);
break;
case HTML:
content.addHtml(sourceString);
break;
case UNDEFINED:
default:
content.addCode(sourceString);
break;
}
return content;
}
private void showTab(String tabUrl) {
Div tab = tabComponents.get(tabUrl);
if (tab != null) {
container.removeAll();
container.add(tab);
navBar.setActive(getTabUrl(tabUrl));
}
}
@Override
public void setParameter(BeforeEvent event,
@OptionalParameter String parameter) {
showTab(parameter == null ? "" : parameter);
}
/**
* Adds a demo that shows how the component looks like with specific
* variants applied.
*
* @param componentSupplier
* a method that creates the component to which variants will be
* applied to
* @param addVariant
* a function that adds the new variant to the component
* @param removeVariant
* a function that removes the variant from the component
* @param variantToThemeName
* function that converts variant to an html theme name
* @param variants
* list of variants to show in the demos
* @param <T>
* variants' type
* @param <C>
* component's type
*/
protected <T extends Enum<?>, C extends Component & HasTheme> void addVariantsDemo(
Supplier<C> componentSupplier, BiConsumer<C, T> addVariant,
BiConsumer<C, T> removeVariant,
Function<T, String> variantToThemeName, T... variants) {
C component = componentSupplier.get();
component.setId(COMPONENT_WITH_VARIANTS_ID);
Div message = new Div();
message.setText(
"Toggle a variant to see how the component's appearance will change.");
Div variantsToggles = new Div();
variantsToggles.setId(VARIANT_TOGGLE_BUTTONS_DIV_ID);
for (T variant : variants) {
if (variant.name().startsWith("LUMO_")) {
String variantName = variantToThemeName.apply(variant);
variantsToggles
.add(new NativeButton(
getButtonText(variantName,
component.getThemeNames()
.contains(variantName)),
event -> {
boolean variantPresent = component
.getThemeNames()
.contains(variantName);
if (variantPresent) {
removeVariant.accept(component,
variant);
} else {
addVariant.accept(component, variant);
}
event.getSource().setText(getButtonText(
variantName, !variantPresent));
}));
}
}
addCard("Theme variants usage", message, component, variantsToggles);
}
private String getButtonText(String variantName, boolean variantPresent) {
return String.format(
variantPresent ? "Remove '%s' variant" : "Add '%s' variant",
variantName);
}
}
package com.example.application.views;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import com.vaadin.flow.demo.SourceCodeExample;
import com.vaadin.flow.demo.SourceCodeExample.SourceType;
/**
* Utility class for obtaining {@link SourceCodeExample}s for classes. Modified
* version of SourceContentResolver for Spring.
*/
public class SpringSourceContentResolver {
// @formatter::off
private static final ConcurrentHashMap<Class<? extends SpringDemoView>, List<SourceCodeExample>> CACHED_SOURCE_EXAMPLES = new ConcurrentHashMap<>();
// @formatter::on
private static final Pattern SOURCE_CODE_EXAMPLE_BEGIN_PATTERN = Pattern
.compile("\\s*// begin-source-example");
private static final Pattern SOURCE_CODE_EXAMPLE_END_PATTERN = Pattern
.compile("\\s*// end-source-example");
private static final Pattern SOURCE_CODE_EXAMPLE_HEADING_PATTERN = Pattern
.compile("\\s*// source-example-heading: (.*)");
private static final Pattern SOURCE_CODE_EXAMPLE_TYPE_PATTERN = Pattern
.compile("\\s*// source-example-type: ([A-Z]+)");
private SpringSourceContentResolver() {
}
/**
* Get all {@link SourceCodeExample}s from a given class.
*
* @param demoViewClass
* the class to retrieve source code examples for
* @return an unmodifiable list of source code examples
*/
public static List<SourceCodeExample> getSourceCodeExamplesForClass(
Class<? extends SpringDemoView> demoViewClass) {
return CACHED_SOURCE_EXAMPLES.computeIfAbsent(demoViewClass,
SpringSourceContentResolver::parseSourceCodeExamplesForClass);
}
private static List<SourceCodeExample> parseSourceCodeExamplesForClass(
Class<? extends SpringDemoView> demoViewClass) {
String resourcePath = getResourcePath(demoViewClass);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
getResource(resourcePath).getInputStream(),
StandardCharsets.UTF_8))) {
List<String> lines = reader.lines().collect(Collectors.toList());
return Collections.unmodifiableList(parseSourceCodeExamples(lines));
} catch (IOException e) {
throw new RuntimeException(
"Error reading source examples for class " + demoViewClass,
e);
}
}
private static String getResourcePath(
Class<? extends SpringDemoView> demoViewClass) {
String javaFile = demoViewClass.getSimpleName() + ".java";
String formattedPackageName = demoViewClass.getPackage().getName()
.replaceAll("\\.", "/");
String resourcePath = formattedPackageName + "/" + javaFile;
if (!isAvailable(resourcePath)) {
resourcePath = "../test-classes/"
+ demoViewClass.getPackage().getName() + "/" + javaFile;
if (!isAvailable(resourcePath)) {
resourcePath = "/" + formattedPackageName + "/" + javaFile;
if (!isAvailable(resourcePath)) {
throw new IllegalArgumentException(
"Could not find file resources for '"
+ demoViewClass.getName() + "'!");
}
}
}
return resourcePath;
}
private static Resource getResource(String resourcePath) {
ResourceLoader resourceLoader = new DefaultResourceLoader();
return resourceLoader.getResource(resourcePath);
}
private static boolean isAvailable(String resourcePath) {
return getResource(resourcePath) != null;
}
private static List<SourceCodeExample> parseSourceCodeExamples(
List<String> sourceLines) {
List<SourceCodeExample> examples = new ArrayList<>();
int startIndex = -1;
int endIndex = -1;
for (int i = 0; i < sourceLines.size(); i++) {
if (SOURCE_CODE_EXAMPLE_BEGIN_PATTERN.matcher(sourceLines.get(i))
.matches()) {
startIndex = i;
} else if (SOURCE_CODE_EXAMPLE_END_PATTERN
.matcher(sourceLines.get(i)).matches()) {
endIndex = i;
}
if (startIndex != -1 && endIndex != -1
&& startIndex + 1 < endIndex) {
examples.add(parseSourceCodeExample(
sourceLines.subList(startIndex + 1, endIndex)));
startIndex = -1;
endIndex = -1;
}
}
return examples;
}
private static SourceCodeExample parseSourceCodeExample(
List<String> sourceLines) {
String heading = parseValueFromPattern(sourceLines,
SOURCE_CODE_EXAMPLE_HEADING_PATTERN, Function.identity(),
() -> null);
SourceType sourceType = parseValueFromPattern(sourceLines,
SOURCE_CODE_EXAMPLE_TYPE_PATTERN, SourceType::valueOf,
() -> SourceType.UNDEFINED);
SourceCodeExample example = new SourceCodeExample();
example.setHeading(heading);
example.setSourceType(sourceType);
example.setSourceCode(
String.join("\n", trimWhitespaceAtStart(sourceLines)));
return example;
}
private static <T> T parseValueFromPattern(List<String> sourceLines,
Pattern pattern, Function<String, T> valueProvider,
Supplier<T> nullValueProvider) {
for (int i = 0; i < sourceLines.size(); i++) {
Matcher matcher = pattern.matcher(sourceLines.get(i));
if (matcher.matches()) {
sourceLines.remove(i);
return valueProvider.apply(matcher.group(1));
}
}
return nullValueProvider.get();
}
private static List<String> trimWhitespaceAtStart(
List<String> sourceLines) {
int minIndent = Integer.MAX_VALUE;
for (String line : sourceLines) {
if (line == null || line.isEmpty()) {
continue;
}
int indent = getWhitespaceCountAtStart(line);
if (indent < minIndent) {
minIndent = indent;
}
}
List<String> trimmed = new ArrayList<>();
for (String line : sourceLines) {
if (line == null || line.isEmpty()) {
trimmed.add("");
} else {
trimmed.add(line.substring(minIndent));
}
}
return trimmed;
}
private static int getWhitespaceCountAtStart(String line) {
int indent = 0;
for (int i = 0; i < line.length(); i++) {
if (!Character.isWhitespace(line.charAt(i))) {
return indent;
}
indent++;
}
return indent;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment