Warning
This is the documentation for the in-development branch of Blueprint. You can find the documentation for previous releases on tags.
Blueprint
Blueprint is a lib for writing fast, reusable and testable HTML templates in plain Crystal, allowing an OOP (Oriented Object Programming) approach when building web views.
class MyForm
include Blueprint::HTML
private def blueprint
div class: "mb-3" do
label(for: "password") { "Password" }
input type: "password", id: "password"
end
end
end
Output:
<div class="mb-3">
<label for="password">Password</label>
<input type="password" id="password">
</div>
Installation
-
Add the dependency to your
shard.yml
:dependencies: blueprint: github: stephannv/blueprint
-
Run
shards install
Changelog
Changelog can be found here
Usage
Basic
You need three things to start using blueprint:
- Require
"blueprint/html"
- Include
Blueprint::HTML
module in your class - Define a
blueprint
method to write an HTML structure inside.
require "blueprint/html"
class ExamplePage
include Blueprint::HTML
private def blueprint
doctype
html do
head do
title { "My website" }
link rel: "stylesheet", href: "app.css"
script type: "text/javascript", src: "app.js"
end
body do
p { "Hello" }
div class: "bg-gray-200" do
label(for: "email") { "Email" }
input type: "text", id: "email"
end
end
end
end
end
With your class defined, you can instantiate it and call to_html
and get the result.
page = ExamplePage.new
puts page.to_html
The output (this HTML output is formatted to improve the visualization):
<!DOCTYPE html>
<html>
<head>
<title>My website</title>
<link rel="stylesheet" href="app.css">
<script type="text/javascript" src="app.js"></script>
</head>
<body>
<p>Hello</p>
<div class="bg-gray-200">
<label for="email">Email</label>
<input type="text" id="email">
</div>
</body>
</html>
Blueprints are just POCOs
Bluprints are Plain Old Crystal Objects (POCOs). You can add any behavior to your class just like a normal Crystal class.
class Profiles::ShowPage
include Blueprint::HTML
def initialize(@user : User); end
private def blueprint
h1 { @user.display_name }
if moderator?
span class: "bg-blue-500" do
img src: "moderator-badge.png"
end
end
end
private def moderator?
@user.role == "moderator"
end
end
page = Profiles::ShowPage.new(user: moderator)
puts page.to_html
Output:
<h1>Jane Doe</h1>
<span class="bg-blue-500">
<img src="moderator-badge.png">
</span>
It is possible even use structs instead classes:
struct Card
include Blueprint::HTML
private def blueprint
div(class: "card") { "Hello" }
end
end
# or using `record` macro
record Alert, message : String do
include Blueprint::HTML
def blueprint
div(class: "alert") { @message }
end
end
card = Card.new
card.to_html # => <div class="card">Hello</div>
alert = Alert.new(message: "Hello")
alert.to_html # => <div class="alert">Hello</div>
Creating components
You can create reusable components using Blueprint, you just need to pass a component instance to the #render
method.
class AlertComponent
include Blueprint::HTML
def initialize(@content : String, @type : String); end
private def blueprint
div class: "alert alert-#{@type}", role: "alert" do
@content
end
end
end
class ExamplePage
include Blueprint::HTML
private def blueprint
h1 { "Hello" }
render AlertComponent.new(content: "My alert", type: "primary")
end
end
page = ExamplePage.new
puts page.to_html
Output:
<h1>Hello</h1>
<div class="alert alert-primary" role="alert">
My alert
</div>
Passing content
Sometimes you need to pass a complex content that cannot be passed through a constructor parameter. To do this, the blueprint
method needs to receive a block (&
) and yield it. Refactoring the previous Alert component example:
class AlertComponent
include Blueprint::HTML
def initialize(@type : String); end
private def blueprint(&)
div class: "alert alert-#{@type}", role: "alert" do
yield
end
end
end
class ExamplePage
include Blueprint::HTML
private def blueprint
h1 { "Hello" }
render AlertComponent.new(type: "primary") do
h4(class: "alert-heading") { "My Alert" }
p { "Alert body" }
end
end
end
page = ExamplePage.new
puts page.to_html
Output:
<h1>Hello</h1>
<div class="alert alert-primary" role="alert">
<h4 class="alert-heading">My alert</h4>
<p>Alert body</p>
</div>
Composing components
Blueprints components can expose some predefined structure to its users. This can be accomplished by defining public instance methods that accept blocks. Refactoring the previous Alert component example:
class AlertComponent
include Blueprint::HTML
def initialize(@type : String); end
private def blueprint(&)
div class: "alert alert-#{@type}", role: "alert" do
yield
end
end
def title(&)
h4(class: "alert-heading") { yield }
end
def body(&)
p { yield }
end
end
class ExamplePage
include Blueprint::HTML
private def blueprint
h1 { "Hello" }
render AlertComponent.new(type: "primary") do |alert|
alert.title { "My Alert" }
alert.body { "Alert body" }
end
end
end
page = ExamplePage.new
puts page.to_html
Output:
<h1>Hello</h1>
<div class="alert alert-primary" role="alert">
<h4 class="alert-heading">My alert</h4>
<p>Alert body</p>
</div>
Conditional rendering
Blueprints can implement a #render?
method, if this method returns false, the blueprint will not be rendered. This allows you extract the logic from component consumer and put in the component itself.
Instead writing this code:
class ArticlePage
include Blueprint::HTML
def initialize(@article: Article); end
private def blueprint
if @article.draft?
render DraftArticleAlert.new(@article)
end
h1 { @article.title }
end
end
You can write this code:
class ArticlePage
include Blueprint::HTML
def initialize(@article: Article); end
private def blueprint
render DraftArticleAlert.new(@article)
h1 { @article.title }
end
end
class DraftArticleAlert
include Blueprint::HTML
def initialize(@article : Article); end
private def blueprint
div class: "alert alert-warning" do
plain "This is a draft. "
end
end
def render?
@article.draft?
end
end
article = Article.new(title: "Hello Blueprint", draft: false)
page = ArticlePage.new(article: article)
puts page.to_html
Output:
<h1>Hello Blueprint</h1>
Enveloping
By overriding the #envelope(&)
method, you can create a wrapper around blueprint content. This is useful when defining layouts for pages, for example.
class MainLayout
include Blueprint::HTML
private def blueprint(&)
html do
body do
yield
end
end
end
end
class BasePage
include Blueprint::HTML
private def envelope(&)
render(MainLayout.new) do
yield
end
end
end
class HomePage < BasePage
private def blueprint
h1 { "Home" }
end
end
page = HomePage.new
puts page.to_html
Output:
<html>
<body>
<h1>Home</h1>
</body>
</html>
If you have more than one layout, you can take advantage of generics:
class MainLayout
include Blueprint::HTML
private def blueprint(&)
html do
body do
div class: "main-layout" do
yield
end
end
end
end
end
class AuthLayout
include Blueprint::HTML
private def blueprint(&)
html do
body do
div class: "auth-layout" do
yield
end
end
end
end
end
class BasePage(T)
include Blueprint::HTML
private def envelope(&)
render(T.new) do
yield
end
end
end
class LoginPage < BasePage(AuthLayout)
private def blueprint
h1 { "Sign In" }
end
end
page = LoginPage.new
puts page.to_html
Output:
<html>
<body>
<div class="auth-layout">
<h1>Sign In</h1>
</div>
</body>
</html>
NamedTuple attributes
If you pass a NamedTuple attribute to some element, it will be flattened with a dash between each level. This is useful for data-*
and aria-*
attributes.
class ExamplePage
include Blueprint::HTML
private def blueprint
div data: { id: 42, target: "#home" }, aria: { selected: "true" } do
"Home"
end
end
end
page = ExamplePage.new
puts page.to_html
Output:
<div data-id="42" data-target="#home" aria-selected="true">
Home
</div>
Boolean attributes
If you pass true
to some attribute, it will be rendered as a boolean HTML attribute, in other words, just the attribute name will be rendered without the value. If you pass false
the attribute will not be rendered. If you want the attribute value to be "true"
or "false"
, use true
and false
between quotes.
class ExamplePage
include Blueprint::HTML
private def blueprint
div required: true, disabled: false, x: "true", y: "false" do
"Boolean"
end
end
end
page = ExamplePage.new
puts page.to_html
Output:
<div required x="true" y="false">
Boolean
</div>
Array attributes
If you pass an Array as attribute value, it will be flattened and joined using " "
as separator.
class ExamplePage
include Blueprint::HTML
BASE_CLASSES = "bg-white shadow border"
TEXT_CLASSES = ["text-lg", "font-medium"]
private def blueprint
div class: [BASE_CLASSES, TEXT_CLASSES] do
"Hello"
end
end
end
page = ExamplePage.new
puts page.to_html
Output:
<div class="bg-white shadow border text-lg font-medium">
Hello
</div>
Utils
You can use the #plain
helper to write plain text on HTML and the #whitespace
helper to add a simple whitespace. The #comment
allows you to write HTML comments.
class ExamplePage
include Blueprint::HTML
private def blueprint
comment { "This is an HTML comment" }
h1 do
plain "Hello"
whitespace
strong { "Jane Doe" }
end
end
end
page = ExamplePage.new
puts page.to_html
Output:
<!--This is an HTML comment-->
<h1>
Hello <strong>Jane Doe</strong>
</h1>
Safety
All content and attribute values passed to elements and components are escaped:
class ExamplePage
include Blueprint::HTML
private def blueprint
span { "<script>alert('hello')</script>" }
input(class: "some-class\" onblur=\"alert('Attribute')")
end
end
page = ExamplePage.new
puts page.to_html
Output:
<span><script>alert('hello')</script></span>
<input class="some-class" onblur="alert('Attribute')">
Custom tags
You can register custom HTML tags using the register_element
macro. The first argument is the helper method and the second argument is an optional tag name.
class ExamplePage
include Blueprint::HTML
register_element :trix_editor
register_element :my_button, "v-btn"
private def blueprint
trix_editor
my_button(to: "#home") { "My button" }
end
end
page = ExamplePage.new
puts page.to_html
Output:
<trix-editor></trix-editor>
<v-btn to="#home">My button</v-btn>
Registering components helpers
Blueprint has the register_component
macro. It is useful to avoid writing the fully qualified name of the component class. Instead writing something like render Views::Components::Forms::LabelComponent.new(for: "password")
you could write just label_component(for: "password")
. You need to include the Blueprint::HTML::ComponentRegistrar
module to make register_component
macro available.
class AlertComponent
include Blueprint::HTML
def initialize(@type : String); end
private def blueprint(&)
div class: "alert alert-#{@type}", role: "alert" do
yield
end
end
def title(&)
h4(class: "alert-heading") { yield }
end
def body(&)
p { yield }
end
end
module ComponentHelpers
include Blueprint::HTML::ComponentRegistrar
register_component :alert_component, AlertComponent
end
class ExamplePage
include Blueprint::HTML
include ComponentHelpers
private def blueprint
h1 { "Hello" }
alert_component(type: "primary") do |alert|
alert.title { "My Alert" }
alert.body { "Alert body" }
end
end
end
Output:
<h1>Hello</h1>
<div class="alert alert-primary" role="alert">
<h4 class="alert-heading">My alert</h4>
<p>Alert body</p>
</div>
Development
TODO Write development instructions here
Contributing
- Fork it (https://github.com/stephannv/blueprint/fork)
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request
Contributors
- Stephann V. - creator and maintainer