Layouting
The main layouting primitive Bonito offers is Grid
, Column
and Row
. They are all based on css display: grid
and Bonito offers a small convenience wrapper around it.
We recommend reading through the great introduction to Styles grids by Josh Comeau: interactive guide to grid, for a better understanding of how grids work. It's recommended to read this before following this tutorial, since the examples are styled much nicer and explain everything in much greater detail.
To easier apply the tutorial to Bonito, we ported all the examples of the tutorial, while only describing them with the bare minimum. To get the full picture, please refer to the linked, original tutorial!
Let's start with the docstring for Grid
:
Bonito.Grid
— FunctionGrid(
elems...;
gap="10px",
width="100%",
height="100%",
# All below Attributes are set to the default CSS values:
columns="none",
rows="none",
areas="none",
justify_content="normal",
justify_items="legacy",
align_content="normal",
align_items="legacy",
style::Styles=Styles(),
div_attributes...,
)
A Grid is a container that lays out its children in a grid, based on the powerful css display: grid
property.
It pretty much just sets the css attributes to some defaults, but everything can be overwritten or extended by passing your own style=Styles(...)
object. All Styles objects inside one App will be merged into a single stylesheet, so using many grids with the same keyword arguments will only generate one entry into the global stylesheet. You can read more about this in the styling section.
There's also Row
and Col
which uses Grid
under the hood:
Bonito.Row
— FunctionRow(elems...; grid_attributes...)
Places objects in a row, based on Grid
.
Bonito.Col
— FunctionCol(elems...; grid_attributes...)
Places objects in a column, based on Grid
.
Implicit Grids
If we don't specify any attributes for the Grid
, the default will be one dynamic column, where every item gets their own row:
DemoCard(content=DOM.div(); style=Styles(), attributes...) = Card(content; backgroundcolor=:silver, border_radius="2px", style=Styles(style, "color" => :white),attributes...)
App() do sess
s = Bonito.Slider(1:5)
cards = map(s.value) do n
cards = [DemoCard(height="50px", width="300px") for i in 1:n]
return Grid(cards;)
end
return Bonito.record_states(sess, Grid(s, cards))
end
If we specify a height, while not specifying a height for the elements, the space will be partitioned for the n children:
App() do sess
s = Bonito.Slider(1:5)
cards = map(s.value) do n
cards = [DemoCard(width="300px") for i in 1:n]
Grid(cards; height="300px")
end
return Bonito.record_states(sess, Grid(s, cards))
end
To introduce new columns, we can specify a width for each column via the column
keyword, which supports any css unit:
App() do
grid = Grid(DemoCard(), DemoCard();
height="300px", columns="25% 75%")
return grid
end
Grid also introduced a new css unit namely fr
, which represents the fraction of the leftover space in the grid container.
To see what that means, the below example shows how the percent based unit will shrink proportionally, while fr
divides the space more evenly and can take the size of the content into it's constraint solver:
App() do sess
container_width = Bonito.Slider(5:0.1:100)
container_width[] = 100
imstyle = Styles(
"display" => :block, "position" => :relative, "width" => "100px",
"max-width" => :none # needs to be set so it's not overwritten by others
)
img = DOM.img(; src="https://docs.makie.org/stable/logo.svg", style=imstyle)
style = Styles("position" => :relative, "display" => :flex, "justify-content" => :center, "align-items" => :center)
function example_grid(cols)
grid = Grid(DemoCard(img; style=style), DemoCard(DOM.div(); style=style); columns=cols)
container = DOM.div(grid; style=Styles("height" => "200px", "width" => "500px"))
return Grid(container; rows="1fr", justify_content="center"), container
end
pgrid, p1 = example_grid("25% 75%")
frgrid, p2 = example_grid("1fr 3fr")
grid_percent = DOM.div(Grid(pgrid; rows="1fr"))
grid_fr = DOM.div(Grid(frgrid; rows="1fr"))
onjs(sess, container_width.value, js"w=> {$(p1).style.width = (5 * w) + 'px';}")
onjs(sess, container_width.value, js"w=> {$(p2).style.width = (5 * w) + 'px';}")
title_percent = DOM.h2("Grid(...; columns=\"25% 75%\")")
title_fr = DOM.h2("Grid(...; columns=\"1fr 3fr\")")
return Grid(container_width, title_percent, grid_percent, title_fr, grid_fr; columns="1fr")
end
Grid(...; columns="25% 75%")
Grid(...; columns="1fr 3fr")
Now, what happens if we add more then 2 items to a Grid with 2 columns?
# Little helper to create a Card with centered content
centered(c; style=Styles(), kw...) = DemoCard(Centered(DOM.h4(c; style=Styles("color" => :white))); style=style, kw...)
App() do
cards = [centered(i) for i in 1:3]
grid = Grid(cards...; columns="1fr 3fr")
return DOM.div(grid; style=Styles("margin" => "20px"))
end
1
2
3
Specifying the size of the rows works exactly the same as with columns
:
App() do
cards = [centered(i) for i in 1:4]
grid = Grid(cards...; columns="1fr 3fr", rows="5rem 1fr")
return DOM.div(grid; style=Styles("width" => "400px", "height" => "300px", "margin" => "20px"))
end
1
2
3
4
Now, if we want to do something with lots of rows/columns, e.g. a calendar, it may get tiring to write those out, which is why css offers the repeat
function:
App() do
cards = [centered(i) for i in 1:31]
grid = Grid(cards...; columns="repeat(7, 1fr)")
return DOM.div(grid; style=Styles("width" => "400px", "margin" => "5px"))
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Assigning children
Children can be assigned slots in the layout explicitly, and it's also possible to assign them to multiple slots. The css syntax for this is:
start_column = 1
end_column = 3
start_row = 1
end_row = 3
style = Styles(
"grid-column" => "$start_column / $end_column",
"grid-row" => "$start_row / $end_row"
)
child = DOM.div(style=style) # assign child from slot 1-2
To illustrate how this works, here is an interactive app where you can select the slots in the grid, and see the corresponding grid/column assignments:
function centered2d(i, j;)
return Card(Centered("($i, $j)"; dataCol="$i,$j", style=Styles("user-select" => :none)))
end
App() do
cards = [centered2d(i, j) for i in 1:4 for j in 1:4]
hover_style = Styles(
"background-color" => :blue,
"opacity" => 0.2,
"z-index" => 1,
"display" => :none,
"user-select" => :none,
)
hover = DOM.div(; style=hover_style)
grid_style = Styles("position" => :absolute, "top" => 0, "left" => 0)
size = "300px"
background_grid = Grid(cards...; columns="repeat(4, 1fr)", gap="0px",
style=grid_style, height=size, width=size)
rows = [DOM.div() for i in 1:15]
selected_grid = Grid(hover, rows...; columns="repeat(4, 1fr)", gap="0px",
style=grid_style, height=size, width=size)
style_display = centered("Styles(...)")
hover_js = js"""
const hover = $(hover);
const grid = $(selected_grid);
const style_display = $(style_display);
const h2_node = style_display.children[0].children[0];
let is_dragging = false;
let start_position = null;
function get_element(e) {
const elements = document.elementsFromPoint(e.clientX, e.clientY);
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
if (element.getAttribute('data-col')) {
return element;
}
}
return
}
function handle_click(current) {
// Check if the current element is a child of the container
const index = current.getAttribute('data-col')
if (index) {
const start = start_position.split(",").map(x=> parseInt(x))
const end = index.split(",").map(x=> parseInt(x))
const [start_row, end_row] = [start[0], end[0]].sort()
const [start_col, end_col] = [start[1], end[1]].sort()
const row = (start_row) + " / " + (end_row + 1)
const col = (start_col) + " / " + (end_col + 1)
hover.style["grid-row"] = row
hover.style["grid-column"] = col
const nelems = 16 - ((end_row - start_row+ 1) * (end_col - start_col+ 1))
for (let i = 0; i < grid.children.length; i++) {
console.log(i)
const child = grid.children[i];
if (child == hover) {
continue
}
child.style.display = nelems >= i ? "block" : "none";
}
h2_node.innerText = 'Styles(\n\"grid-row\" => \"' + row + '\",\n \"grid-column\" => \"' + col + '\"\n)';
}
}
grid.addEventListener('mousedown', (e) => {
if (!hover) {
return
}
is_dragging = true;
const current = get_element(e);
const index = current.getAttribute('data-col')
if (!index) {
return;
}
start_position = index
hover.style.display = "block"; // unhide hover element
handle_click(current);
});
grid.addEventListener('mousemove', (e) => {
if (!is_dragging) return;
const current = get_element(e);
handle_click(current);
});
document.addEventListener('mouseup', () => {
is_dragging = false;
});
"""
grids = DOM.div(background_grid, selected_grid, hover_js; style=Styles("position" => :relative, "height" => size))
return Grid(style_display, grids; columns="1fr 2fr", width="100%")
end
Styles(...)
Grid areas
We can now easily create complex layouts like this:
App() do
sidebar = DemoCard(
"SIDEBAR",
style = Styles(
"grid-column" => "1",
"grid-row" => "1 / 3",
)
)
header = DemoCard(
"HEADER",
style = Styles(
"grid-column" => "2",
"grid-row" => "1",
)
)
main = DemoCard(
"MAIN",
style = Styles(
"grid-column" => "2",
"grid-row" => "2",
)
)
grid = Grid(
sidebar, header, main,
columns = "2fr 5fr",
rows = "50px 1fr"
)
return DOM.div(grid; style=Styles("height" => "600px", "margin" => "20px", "position" => :relative))
end
With areas
, in css grid-template-areas
this can be made even simpler:
App() do
sidebar = DemoCard(
"SIDEBAR",
style = Styles("grid-area" => "sidebar")
)
header = DemoCard(
"HEADER",
style = Styles("grid-area" => "header")
)
main = DemoCard(
"MAIN",
style = Styles("grid-area" => "main")
)
grid = Grid(
sidebar, header, main,
columns = "2fr 5fr",
rows = "50px 1fr",
areas = """
'sidebar header'
'sidebar main';
"""
)
return DOM.div(grid; style=Styles("height" => "600px", "margin" => "20px", "position" => :relative))
end
The syntax is quite similar to Julia's matrix syntax, just wrapping all rows into '...row...'
! To span multiple rows or columns, the name can be repeated multiple times.
Alignment
App() do
grid = Grid(
DemoCard(), DemoCard(),
columns = "90px 90px",
)
return DOM.div(grid; style=Styles("height" => "200px", "margin" => "20px", "position" => :relative, "background-color" => "#F88379", "padding" => "5px"))
end
App() do session
justification = Bonito.Dropdown(["space-evenly", "center", "end", "space-between", "space-around", "space-evenly"], style=Styles("width" => "200px"))
grid = Grid(
DemoCard(), DemoCard(),
columns = "90px 90px"
)
onjs(session, justification.option_index, js""" (idx) => {
grid = $(grid)
grid.style["justify-content"] = $(justification.options[])[idx-1]
}""")
area_style = Styles("height" => "200px", "width" => "600px", "margin" => "20px", "position" => :relative, "background-color" => "#F88379", "padding" => "5px")
grid_area = DOM.div(grid; style=area_style)
return Grid(justification, grid_area; justify_items="center")
end
App() do session
content = Bonito.Dropdown(["space-evenly", "center", "end", "space-between", "space-around"])
items = Bonito.Dropdown(["stretch", "start", "center", "end"])
grid = Grid(
DemoCard(), DemoCard(),
DemoCard(), DemoCard(),
columns = "90px 90px"
)
onjs(session, content.option_index, js""" (idx) => {
grid = $(grid)
const val = $(content.options[])[idx-1]
grid.style["justify-content"] = val
}""")
onjs(session, items.option_index, js""" (idx) => {
grid = $(grid)
const val = $(items.options[])[idx-1]
grid.style["justify-items"] = val
}""")
grid_area = DOM.div(grid; style=Styles("height" => "200px", "width" => "600px", "margin" => "20px", "position" => :relative, "background-color" => "#F88379", "padding" => "5px",
"grid-column" => "1 / 3", "grid-row" => "2"))
return Grid(content, items, grid_area; width="500px", justify_items="space-around")
end
App() do session
content = Bonito.Dropdown(["space-evenly", "center", "end", "space-between", "space-around"])
items = Bonito.Dropdown(["stretch", "start", "center", "end"])
align_content = Bonito.Dropdown(["space-evenly", "center", "end", "space-between", "space-around"])
align_items = Bonito.Dropdown(["stretch", "start", "center", "end"])
grid_style = Styles("position" => :absolute, "top" => 0, "left" => 0)
grid = Grid(
centered("One"), centered("Two"),
centered("Three"), centered("Four"),
columns = "100px 100px",
rows = "100px 100px",
style=grid_style
)
grid_col() = DOM.div(style=Styles("border" => "2px dashed white", "width"=>"100px"))
grid_row() = DOM.div(style=Styles("border" => "2px dashed white", "height"=>"100px"))
shadow_cols = Grid(
grid_col(), grid_col(),
columns = "100px 100px",
style=grid_style
)
shadow_rows = Grid(
grid_row(), grid_row(),
rows = "100px 100px",
style=grid_style
)
onjs(session, content.option_index, js""" (idx) => {
const grids = [$(grid), $(shadow_cols)]
const val = $(content.options[])[idx-1]
grids.forEach(x=> x.style["justify-content"] = val)
}""")
onjs(session, items.option_index, js""" (idx) => {
const grids = [$(grid), $(shadow_cols)]
const val = $(items.options[])[idx-1]
grids.forEach(x=> x.style["justify-items"] = val)
}""")
onjs(session, align_content.option_index, js""" (idx) => {
const grids = [$(grid), $(shadow_rows)]
const val = $(align_content.options[])[idx-1]
grids.forEach(x=> x.style["align-content"] = val)
}""")
onjs(session, align_items.option_index, js""" (idx) => {
const grids = [$(grid), $(shadow_rows)]
const val = $(align_items.options[])[idx-1]
grids.forEach(x=> x.style["align-items"] = val)
}""")
grid_area = DOM.div(grid, shadow_cols, shadow_rows; style=Styles(
"height" => "400px", "width" => "400px",
"margin" => "20px",
"padding" => "5px",
"position" => :relative,
"background-color" => "#F88379",
"grid-column" => "1 / 3", "grid-row" => "4"))
text(t) = DOM.div(t; style=Styles("font-size" => "1.3rem", "font-weight" => "bold"))
final_grid = Grid(
text("Row Alignment"), text("Col Justification"),
align_content, content,
align_items, items,
grid_area;
rows = "2rem 1rem 2rem 1fr",
align_items = "center",
justify_items="begin", justify_content="center",
width="400px")
return DOM.div(final_grid, style=Styles("padding" => "20px"))
end