Skip to main content

Templates

Hokusai Markup Language

Hokuasi uses a custom tree-sitter grammar for describe a node tree called HML (Hokusai Markup Language)

Templates are declared on blocks, which are classes that inherit from Hokusai::Block

note

When describing a template, the first directive must be [template]

Describing Nodes

Nodes are described using a whitespace significant markup language, with child nodes indented under parent nodes.

For example

[template]
parent
child
grand-child
grand-child2
sibling

Node names can be anything, but names and values must be declared on a block with Hokusai::Block::uses

The above might look like

require_relative "./parent_block"
require_relative "./child_block"
require_relative "./grandchild_block"
require_relative "./sibling"

class RelationshipBlock < Hokusai::Block
template <<~EOF
[template]
parent
child
grand_child
grand_child2
sibling
EOF

# the keys to uses map to the node name,
# and the values are classes that inherit `Hokusai::Block`
uses(
parent: ParentBlock,
child: ChildBlock,
grand_child: GranchildBlock,
grand_child2: GrandchildBlock,
sibling: SiblingBlock
)
end

Reserved Template keywords

There are a couple of reserved template keywords that should not be used.

namepurpose
virtualmeant for marking a template as a no-op, useful for blocks that render themselves using the drawing api
slotmeant for declaring a child as a slot. Slots allow one to compose reusable blocks that can have different children or behaviors

Drawing Api

Blocks don't always need to use templates. Hokusai provides an API for drawing directly to the screen.

Let's say we want to make a block that renders a circle without using any built-in blocks / templates.

class Circle < Hokusai::Block
# mark the template as virtual
template <<~EOF
[template]
virtual
EOF

# Render takes a `Hokusai::Canvas`, processes any draw commands,
# and yields the canvas to the next block.
def render(canvas)
# the canvas suggests _where_ we should draw based the layout that this block is used in.
# we want to render a circle in the center of the canvas.
x = canvas.x + (canvas.width / 2)
y = canvas.y + (canvas.height / 2)

# the `#circle` command also takes a radius of the circle
radius = 5.0

# draw starts the drawing api
# multiple commands can be called inside the draw block
# and can render rectangles, text, images, and clipping regions.
draw do
circle(x, y, radius) do |command|
# each command may have additional properties that can be set
command.color = Hokusai::Color.new(255, 0, 0)
end
end

# finally yield the canvas so that the next components can draw
yield canvas
end
end

This block can be used as any other and does the following

  • Declares its template as virtual
  • Declares a render method
  • Calls the draw method inside render and calls any commands
info

It can be more performant to render complex components directly than to use templated blocks.

Props

Template nodes are always mapped to a block, and those blocks can declare expected properties to be passed to it from the target that uses it.

Props can be static or dynamic, and are declared as such.

Let's make our Circle more extensible with props and use it in another component

# circle.rb
class Circle < Hokusai::Block
template <<~EOF
[template]
virtual
EOF

# the computed class method tells us that we expect a prop
# and auto-generates an instance method for it.
computed :color, default: [255, 0, 0], convert: Hokusai::Color
computed :radius, default: 10.0, convert: proc(&:to_f)

def render(canvas)
x = canvas.x + (canvas.width / 2)
y = canvas.y + (canvas.height / 2)

draw do
# we can call `color` and `radius` directly now
circle(x, y, radius) do |command|
command.color = color
end
end

yield canvas
end
end

The ::computed method allows us to declare expected props as well as change their type. These props can then be used directly in any of the block's instance methods.

require_relative "./circle"

class ColoredCircles < Hokusai::Block
template <<~EOF
[template]
blue_circle { color="0,0,255" }
red_circle { :color="get_red_color" }
EOF

uses(
blue_circle: Circle,
red_circle: Circle
)

def get_red_color
[255, 0, 0]
end
end

The above component draws a blue circle above a red circle.

static prop content (without a colon) are passed to the child as a string,

dynamic prop content (starting with a colon) is evaluated in the context of the instance.

Events

Like modern web frameworks, Hokusai also supports both builtin and custom events.

Event attributes are declared with props except begin with an @ symbol.

info

Hokusai supports some builtin input events.

the handlers attached to these events will receive the following objects as their parameter.

EventObject
@clickHokusai::ClickEvent
@hoverHokusai::HoverEvent
@mousemoveHokusai::MouseMoveEvent
@mouseupHokusai::MouseUpEvent
@mousedownHokusai::MouseDownEvent
@mouseoutHokusai::MouseOutEvent
@wheelHokusai::WheelEvent
@keypressHokusai::KeyPressEvent
@keyupHokusai::KeyUpEvent

Lets make a Circle component that changes color when it is clicked

require "./circle"

class ColorChangingCircle < Hokusai::Block
template <<~EOF
[template]
circle {
:color="circle_color"
@click="change_color"
}
EOF

def circle_color
@circle_color || [255,255,255]
end

def change_color(_event)
@circle_color = [
[255,0,0],
[0,255,0],
[0,0,255]
].sample
end
end

In the above component, we pass a dynamic color prop based on the value of @circle_color

In the @click handler, we set @circle_color to a random value of red, blue, or green.

The new value will be passed to the child on the next render.

Styles

Most of the time, props passed to a block are going to be static declarations used to describe styles.

Unlike HTML/CSS however, we can not know what the props are going to be for a given component ahead of time.

color might work for one component, while background might be used for another.

A block can declare a style template that declares any static prop values, including by not limited to colors, sizes, padding, and text content.

Let's make a circle that is defined by a style template.

require "./circle"

class RedCircle < Hokusai::Block
style <<~EOF
[style]
circleStyle {
color: rgb(255,0,0);
radius: 8.0;
}
EOF

template <<~EOF
[template]
circle { ...circleStyle }
EOF

uses(circle: Circle)
end

One might notice that the style templates look a lot like css. The difference is that the keys are arbitrary prop names.

The style can be included into the template node attributes with the ...#{styleName} operator.

info

Unlike static props, style prop values are typed, and will pass the expected type to the child.

Style props values can also call a set of predefined functions, which will return some specific types.

functionreturn value
rgb(red, green, blue, alpha);Hokusai::Color
outline(top, right, bottom, left);Hokusai::Outline
padding(top, right, bottom, left);Hokusai::Padding

Directives

Hokusai supports a couple of directives to conditionally or iteratively render children.

Directives are contained withiin brackets [].

Conditionals

Conditional directives are used in a template like this:

require "./circle"

class ConditionalCircle < Hokusai::Block
template <<~EOF
[template]
vblock { @hover="set_hover" @mouseout="unset_hover" }
[if="hovered"]
red_circle { color="255,0,0" }
[else]
blue_circle { color="0,0,255" }
EOF

uses(
vblock: Hokusai::Blocks::Vblock,
red_circle: Circle,
blue_circle: Circle
)

def hovered
@hovered || false
end

def set_hover(_event)
@hovered = true
end

def unset_hover(_event)
@hovered = false
end
end

The above block will render a red circle if it is hovered, otherwise a blue one.

warning
  • Directives should be indented as if they were the child.
  • Conditional or Looping Directives can not currently be declared at the top-level.

For Loops

For loops will allow us to iterate over an array of values and render components for them.

They are used like this:

require "./circle"

class ColoredCircles < Hokusai::Block
template <<~EOF
[template]
vblock
[for="color in colors"]
circle { :color="color" }
EOF

uses(
vblock: Hokusai::Blocks::Vblock,
circle: Circle
)

def colors
[
[255, 0, 0],
[0, 255, 0],
[0, 0, 255]
]
end
end

This block vertically renders 3 circles of different colors.

It is also possible to pass the directive index and value to a method on the using instance.

require "./circle"

class ColoredCircles < Hokusai::Block
template <<~EOF
[template]
vblock
[for="color in colors"]
circle {
:color="color"
:radius="get_radius(color, index)"
}
EOF

uses(
vblock: Hokusai::Blocks::Vblock,
circle: Circle
)

# the loop directive value and index can be passed as parameters
# to any dynamic prop handlers
def get_radius(color, index)
5 * index
end

def colors
[
[255, 0, 0],
[0, 255, 0],
[0, 0, 255]
]
end
end

This block will render consecutively bigger circles.