A feature-rich Java framework for server-side rendering. Stir allows you to generate UIs using simple Java objects resembling POJOs, document templating, and element descriptors. See the Java Docs for more information.
- Installation
- Building a Simple Page Using Java Objects
- Supported Elements
- Element Descriptors
- Document Templating
- Creating Custom Elements or Components
- Caching Support
- Bootstrap Integration
- Water CSS Integration
- Document Querying
- Examples
This framework can be added to a project using the following Maven dependency:
<dependency>
<groupId>io.john-amiscaray.stir</groupId>
<artifactId>stir</artifactId>
<version>1.0.0</version>
</dependency>
The most basic way to generate pages with Stir would be with the HTMLDocument
class and the Stir provided element classes. Using simple builder patterns and built-in classes representing HTML elements, you can generate large amounts of reusable markup for your applications:
import io.john.amiscaray.stir.domain.HTMLDocument;
import io.john.amiscaray.stir.domain.elements.*;
public class Main {
public static void main(String[] args) {
HTMLDocument document = HTMLDocument
.builder()
.title("My Cool Web Page")
.element(new Nav(NavLinkList.builder()
.navLink(NavLink.fromLabelAndHref("Home", "/home"))
.navLink(NavLink.fromLabelAndHref("Products", "/products"))
.navLink(NavLink.fromLabelAndHref("About Us", "/about"))
.build()))
.element(Article.builder()
.child(Heading.builder()
.content("Welcome to my web page")
.level(1)
.build())
.child(new Paragraph("Lorem Ipsum something something"))
.build())
.withBootStrap(true)
.withBootStrapPopper(true)
.build();
System.out.println(document.generateDocumentString());
}
}
The above Java code outputs the following markup:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Cool Web Page</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
</head>
<body>
<nav>
<ul>
<li>
<a href="/home">
Home
</a>
</li>
<li>
<a href="/products">
Products
</a>
</li>
<li>
<a href="/about">
About Us
</a>
</li>
</ul>
</nav>
<article>
<h1>
Welcome to my web page
</h1>
<p>
Lorem Ipsum something something
</p>
</article>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.min.js" integrity="sha384-mQ93GR66B00ZXjt0YO5KlohRA5SY2XofN4zfuZxLkoj1gXtW8ANNCe9d5Y3eG5eD" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous"></script>
</body>
</html>
The backbone for how all this markup gets generated is the ElementProcessor
class whose role is to take elements using its getMarkup
method or styles with its processStyle
method and generate the appropriate HTML or CSS. Use of this class should be handled within the HTMLDocument
class but if you need to use it manually, you can get an instance of it using the static getInstance
method.
While this example looks slightly verbose, as you'll see later, we provide tools like element descriptors and document templating to reduce the amount of code required.
As of the latest version, Stir has built-in support for the following elements:
- Anchor
- Article
- Button
- Div
- Footer
- Form
- Header
- Heading
- HGroup
- IFrame
- Img
- Input
- Linked CSS styles
- List items
- Meta tags
- Nav
- Nav links
- Ordered List
- Paragraph
- Script
- Section
- Select (with options)
- Style
- Table
- Unordered list
See the Javadocs in this repository for more info on the classes for each of these elements
Version 0.5.0 introduced a new compact syntax for generating HTML markup using a language similar to CSS selectors. These are known as element descriptors. Element descriptors allow you to initialize an AbstractUIElement
using a string following Stir's element descriptor language. To do this, simply call the static ElementDescriptorProcessor.element
method with the element descriptor as a string. As an example, take the following code:
import io.john.amiscaray.stir.domain.elements.Form;
// Import all the static methods of the ElementDescriptorProcessor class so we can use the methods as simple function calls.
import static io.john.amiscaray.stir.util.ElementDescriptorProcessor.*;
class Example {
private Form myForm = (Form) element("form#my-form.red.blue.green[action='/login',method='post']{input[type='text'],input[type='password']}");
}
This is equivalent to:
import io.john.amiscaray.stir.domain.elements.Form;
class Example {
private Form myForm = Form.builder()
.cssClasses(new ArrayList<>(List.of("red", "blue", "green")))
.action("/login")
.method("post")
.id("my-form")
.field(Input.builder()
.type("text")
.build())
.field(Input.builder()
.type("password")
.build());
}
Here, content enclosed in square brackets describe the HTML attributes for the form and the content enclosed in curly brackets describe the inner children of the form. Element descriptors may also describe the inner text content of an element like so:
element("p.red.blue.green('This is the content!')");
which is equivalent to:
Paragraph.builder()
.cssClass("red")
.cssClass("blue")
.cssClass("green")
.content("This is the content!")
.build();
For additional info on element descriptors, see the v0.5.0 release notes.
Note that v0.5.0 is an unstable pre-release and should not be used. In that version, there is a bug which causes the element descriptors to fail in production. This is fixed in v0.5.1. The release notes, however, give a detailed explanation on element descriptors with content not mentioned here.
As of version 0.3.0, Stir comes with a simple templating language for your HTML documents, so you can interpolate Stir generated markup into an HTML template. To do this, you can place keywords into a templating block (delimited by an opening <&
tag and a closing &>
tag). Different keywords can specify different content you set using the HTMLDocument
's builder. As an example, see the following code:
import io.john.amiscaray.stir.domain.HTMLDocument;
import io.john.amiscaray.stir.domain.elements.*;
public class Main {
private final Div sampleContent = Div.builder()
.child(new Heading(1, "Hello World"))
.child(new Paragraph("This is the content"))
.build();
public static void main(String[] args){
HTMLDocument doc = HTMLDocument.builder()
.format("""
<!DOCTYPE html>
<html lang="<& str_lang &>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<& str_meta &>
<& str_hscripts &>
<& str_lstyles &>
<& str_styles &>
<title><& str_title &></title>
</head>
<body>
<hgroup>
<h1><& str_title &></h1>
<h2>Wowo Much Cool!</h2>
</hgroup>
<div class="content-wrapper">
<& str_content &>
</div>
<& str_fscripts &>
</body>
</html>""")
.element(sampleContent)
.title("Hello World")
.language("fr")
.footerScript(new Script("./main.js"))
.headerScript(new Script("./script.js"))
.linkedStyle(new LinkedStyle("./styles.css"))
.style(new Style("""
div {
color: red;
}
"""))
.metaTag(Meta.builder()
.content("thing")
.name("sample")
.build())
.build();
System.out.println(doc.generateDocumentString());
}
}
This prints the following HTML markup
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="sample" content="thing">
<script src="./script.js"></script>
<link href="./styles.css" rel="stylesheet">
<style>
div {
color: red;
}
</style>
<title>Hello World</title>
</head>
<body>
<hgroup>
<h1>Hello World</h1>
<h2>Wowo Much Cool!</h2>
</hgroup>
<div class="content-wrapper">
<div>
<h1>
Hello World
</h1>
<p>
This is the content
</p>
</div>
</div>
<script src="./main.js"></script>
</body>
</html>
Below is a list of all the built-in formatting keywords and their meanings:
str_content
: All the elements added to the documentstr_title
: The title of the pagestr_meta
: The meta tags for the pagestr_hscripts
: The header scripts for the pagestr_fscripts
: The footer scripts for the pagestr_lang
: The language of the pagestr_styles
: A style tag added to the page (set using the HTMLDocument builder style methodstr_lstyles
: Linked style sheets for the page
By default, if no format is set for the document, the following layout will be used:
<!DOCTYPE html>
<html lang="<& str_lang &>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<& str_meta &>
<title><& str_title &></title>
<& str_hscripts &>
<& str_lstyles &>
<& str_styles &>
</head>
<body>
<& str_content &>
<& str_fscripts &>
</body>
</html>
Since version 0.8.0, there is an option to limit the scope of your template to only be for the document body. Simply set the isFormatForBody
option to true in the HTMLDocument
builder. The outer content of the template should match the default template shown above.
From version 0.4.1 onwards, you can now add formatting arguments to your template, allowing you to interpolate custom content into your document format using formatting blocks. To do so, all you need to do is call the formatArgs
method of the HTMLDocument
's builder class with a key and value. The key is what you can use in any formatting block to specify to the FormatProcessor
that you want the corresponding value to be placed in the given area of the template. For example take the following Java code:
import io.john.amiscaray.stir.domain.HTMLDocument;
import io.john.amiscaray.stir.domain.elements.*;
import java.util.LinkedHashMap;
import java.util.List;
import io.john.amiscaray.stir.stub.StudentWithTableAnnotation;
public class Main {
private final Div sampleContent = Div.builder()
.child(new Heading(1, "Hello World"))
.child(new Paragraph("This is the content"))
.build();
public static void main(String[] args){
LinkedHashMap<String, String> navMap = new LinkedHashMap<>();
navMap.put("Home", "/home");
navMap.put("Products", "/products");
navMap.put("About", "/about");
List<StudentWithTableAnnotation> students = List.of(
new StudentWithTableAnnotation(1, "Susan", 4.0f),
new StudentWithTableAnnotation(2, "Karen", 0.1f),
new StudentWithTableAnnotation(3, "George", 3.2f)
);
HTMLDocument doc = HTMLDocument.builder()
.format("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><& str_title &></title>
</head>
<body>
<& nav &>
<h1><& str_title &></h1>
<& str_content &>
</body>
</html>""")
.title("My Document")
.formatArg("nav", Nav.fromLabelHrefMap(navMap))
.element(Div.builder()
.id("main-content")
.child(new Table(students, StudentWithTableAnnotation.class))
.build())
.build();
System.out.println(doc.generateDocumentString());
}
}
Where the StudentWithTableAnnotation
class is defined as so:
package io.john.amiscaray.stir.stub;
import io.john.amiscaray.stir.annotation.TableData;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class StudentWithTableAnnotation implements Comparable{
@TableData(columnName = "Student ID")
private Integer studentId;
@TableData(columnName = "Name")
private String name;
@TableData(columnName = "GPA")
private Float gpa;
@Override
public int compareTo(Object o) {
return ((StudentWithTableAnnotation) o).studentId.compareTo(this.studentId);
}
}
The above code outputs the following markup:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Document</title>
</head>
<body>
<nav>
<ul>
<li>
<a href="/home">
Home
</a>
</li>
<li>
<a href="/products">
Products
</a>
</li>
<li>
<a href="/about">
About
</a>
</li>
</ul>
</nav>
<h1>My Document</h1>
<div id="main-content">
<table>
<thead>
<tr>
<th>Student ID</th>
<th>Name</th>
<th>GPA</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Susan</td>
<td>4.0</td>
</tr>
<tr>
<td>2</td>
<td>Karen</td>
<td>0.1</td>
</tr>
<tr>
<td>3</td>
<td>George</td>
<td>3.2</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
Format args with a value of an AbstractUIElement
will have the corresponding markup of that element be added to the format. Likewise, args of a List of AbstractUIElement
instances will have the markup of each instance be added sequentially. Otherwise, the format processor will call the toString
method of whatever value the argument is.
Element Descriptors can be added in templating blocks to render the HTML of the corresponding object directly in the template. Simply use the following syntax: element(<element descriptor>)
. For example:
FormatProcessor formatProcessor = FormatProcessor.getInstance();
HTMLDocument doc = HTMLDocument.builder()
.format("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
</head>
<body>
<&
element(ul#my-list.red.blue.green{li('red'),li('blue'),li('green')})
element(p#hello-world.red.blue.green('This is the content!'))
&>
</body>
</html>
""")
.build();
System.out.println(formatProcessor.processDocument(doc));
outputs:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
</head>
<body>
<ul id="my-list" class="red blue green">
<li>
red
</li>
<li>
blue
</li>
<li>
green
</li>
</ul>
<p id="hello-world" class="red blue green">
This is the content!
</p>
</body>
</html>
This framework provides custom annotations for creating your own elements or components if the need arises. Every class representing HTML elements must be annotated with the @HTMLElement
annotation so the ElementProcessor
class knows what to do with it. This annotation has options such as the tag name and whether the element type requires a closing tag. Additionally, every element must extend the AbstractUIElement
class which contains a list of CSS classes, an element ID, a boolean hidden
field, and comes with caching support from the CacheableElement
class.
Attributes for your HTML element can be specified using the @Attribute
annotation on any field you wish to use as an attribute. With this annotation, you can specify the name of the attribute and a default attribute value to use in case of a null field.
Child elements can be added using the @ChildList
annotation, the @Nested
annotation, or a combination of either. @ChildList
marks any List
field that contains children of the HTML element, whereas the @Nested
field marks a single HTML element field as a child of the HTML element representing the class.
The inner content of the HTML element can be specified with the @InnerContent
annotation. This marks a field as being the inner content of the HTML element.
The @Label
annotation can be used to mark a property as being the inner content of a label for the given HTML element. From this, the ElementProcessor
class can generate an HTML label element for the corresponding HTML element. For this label to be generated, however, an ID must be set on the object representing the HTML element.
As an optimization to the HTML generation, we added support for the caching of HTML content using the CacheableElement
class. This class gets inherited by all the built-in elements in this framework giving them a cacheContents
field, a cacheStatus
field describing the cache freshness, and a cacheDisabled
field for disabling caching on that element. Upon first generating the HTML content for a given object, the ElementProcessor
class will fill the element's cache contents for use in the next call. The CacheableElement
class updates the cache status of the element every time the cache is written to, and when a setter is called on the original object using a callback function. An event can be triggered for the callback function using the propertyChangeSupport
property of the CacheableElement
class using its firePropertyChange
method from the PropertyChangeSupport
class. Thus, it's important to note that the firePropertyChange
method must be called in the setter of an HTMLElement for proper caching.
To disable caching for an element, update the cacheDisabled
field using the appropriate setter.
The HTMLDocument
class contains a couple of fields which adds the Bootstrap script(s) and stylesheet to your HTML document. The withBootStrap
field flags that the basic script and stylesheet for Bootstrap should be added to the document, whereas the withBootStrapPopper
field is used to add the Popper library needed for specialized Bootstrap elements. Each of these fields may be set using the class' builder.
Note that if you are using a custom template for your HTMLDocument, the
str_lstyles
andstr_fscripts
keywords must be added to the template for these elements to appear. Refer here for more about document templating.
Similar to the Bootstrap integration, Stir comes with support for Water CSS. This can be turned on using the withWaterCSS
method of the HTMLDocument
class' builder, along with a color theme using the waterCSSTheme
method.
The HTMLDocument
class provides support for querying its elements using the familiar querySelector
method which takes a CSS query and returns a list of all the elements added to the document which match that query. At the time of writing this, the querySelector
method supports every CSS query feature except for pseudo-classes and pseudo-elements.
Example uses of this framework can be found here.