The fact that tag literals generate real dom nodes means that we can add/remove/modify the dom in an imperative way. In theory.
# [preview=lg]
import 'util/styles'
# ---
let array = ["First","Second"]
let view = <main>
<button @click=array.push('More')> 'Add'
<ul.list> for item in array
<li> item
# view is a real native DOM element
document.body.appendChild view
Even though we rendered a dynamic list of items, it won't update if new items are added to the array or if members of the array change. Clicking the button will actually add items, but our view is clearly not keeping up. What to do?
To make the tag tree update when our data changes, we need to add pass the tree to imba.mount
.
import 'util/styles'
# ---
let array = ["First","Second"]
imba.mount do
<main>
<button @click=array.push('More')> 'Add'
<ul.list> for item in array
<li> item
Now you will see that when you click the button, our view instantly updates to reflect the new state. How does this happen without a virtual dom? The array is not being tracked in a special way (it is just a plain array), and we are only dealing with real dom elements, which are only changed and updated when there is real need for it. Imba uses a technique we call memoized dom
, and you can read more about how it works here. Here is a more advanced example with more dynamic data and even dynamic inline styles:
# [preview=lg]
import 'util/styles'
css div pos:absolute d:block inset:0 p:4
css mark pos:absolute
css li d:inline-block px:1 m:1 rd:2 fs:xs bg:gray1 @hover:blue2
# ---
let x = 20
let y = 20
let title = "Hey"
imba.mount do
<main @mousemove=(x=e.x,y=e.y)>
<input bind=title>
<label> "Mouse is at {x} {y}"
<mark[x:{x} y:{y} rotate:{x / 360}]> "Item"
<ul> for nr in [0 ... y]
<li> nr % 12 and nr or title
By default Imba will render your whole application whenever anything may have changed. Imba isn't tracking anything. This sounds insane right? Isn't there a reason for all the incredibly complex state management libraries and patterns that track updates and wraps your data in proxies and all that? As long as you have mounted your root element using imba.mount
you usually don't need to think more about it.
The default approach of Imba is to re-render the mounted application after every handled DOM event. If a handler is asynchronous (using await or returning a promise), Imba will also re-render after the promise is finished. Practically all state changes in applications happen as a result of some user interaction.
In the few occasions where you need to manually make sure views are updated, you should call imba.commit
. It schedules an update for the next animation frame, and things will only be rerendered once even if you call imba.commit
a thousand times. It returns a promise that resolves after the actual updates are completed, which is practical when you need to ensure that the view is in sync before doing something.
socket.addEventListener('message',imba.commit)
Calling imba.commit
after every message from socket will ensure that your views are up-to-date when your state changes as a result of some socket message.
def load
let res = await window.fetch("/items")
state.items = await res.json!
imba.commit!