blueprint

Write reusable and testable HTML templates in plain Crystal dsl html oop hacktoberfest
0.1.0 released

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

  1. Add the dependency to your shard.yml:

    dependencies:
      blueprint:
        github: stephannv/blueprint
    
  2. 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>&lt;script&gt;alert(&#39;hello&#39;)&lt;/script&gt;</span>

<input class="some-class&quot; onblur=&quot;alert(&#39;Attribute&#39;)">

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

  1. Fork it (https://github.com/stephannv/blueprint/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Contributors

blueprint:
  github: gunbolt/blueprint
  version: ~> 0.1.0
License MIT
Crystal 1.7.3

Dependencies 0

Development Dependencies 0

Dependents 0

Last synced .
search fire star recently