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
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.
name | purpose |
---|---|
virtual | meant for marking a template as a no-op, useful for blocks that render themselves using the drawing api |
slot | meant 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 insiderender
and calls any commands
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.
Hokusai supports some builtin input events.
the handlers attached to these events will receive the following objects as their parameter.
Event | Object |
---|---|
@click | Hokusai::ClickEvent |
@hover | Hokusai::HoverEvent |
@mousemove | Hokusai::MouseMoveEvent |
@mouseup | Hokusai::MouseUpEvent |
@mousedown | Hokusai::MouseDownEvent |
@mouseout | Hokusai::MouseOutEvent |
@wheel | Hokusai::WheelEvent |
@keypress | Hokusai::KeyPressEvent |
@keyup | Hokusai::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.
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.
function | return 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.
- 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.