Blueprint
Bluprint is a lib for writing fast, reusable and testable HTML templates in plain Crystal, allowing an OOP (Oriented Object Programming) approach when building your views.
class MyForm
include Blueprint::HTML
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
Usage
Basic
You need three things to start using blueprint:
- Require
"blueprint"
- Include
Blueprint::HTML
module into your class - Define a
blueprint
method to write an HTML structure inside.
require "blueprint"
class ExamplePage
include Blueprint::HTML
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
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>
Creating components
You can create reusable components using Blueprint, you just need to pass an component instance to the #render
method.
class AlertComponent
include Blueprint::HTML
def initialize(@content : String, @type : String); end
def blueprint
div class: "alert alert-#{@type}", role: "alert" do
@content
end
end
end
class ExamplePage
include Blueprint::HTML
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
def blueprint(&)
div class: "alert alert-#{@type}", role: "alert" do
yield
end
end
end
class ExamplePage
include Blueprint::HTML
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
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
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>
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
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
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>
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
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
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"
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
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
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