Created
July 8, 2024 12:09
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <dependency> | |
| <groupId>com.vaadin</groupId> | |
| <artifactId>flow-component-demo-helpers</artifactId> | |
| <version>9.0.13</version> | |
| </dependency> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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