Barbecue: a Reactive Data Binder
May 29, 2024I'm on vacation, and my project for this week is to get my blog up and running. One side project for this is to bring my other JavaScript projects into an accessible and well-presented website, chiefly among them is "Monster Vat", a D&D toy which can create parametric monsters. That's a topic for another time.
One feature of the Monster Vat is you can type into a text box, and the text will appear in the monster's stat block on the right. It's a big mess of JavaScript. I've heard React is a good solution for this, but
- I've never used React.
- I barely use JavaScript.
- It can't be too hard, right?
I hacked on it this week, and I've got a pretty good thing going I think.
A taste of barbecue
Type in the text box. Either one. You should see it show up on the other one, and also in a
<pre>
block down below.
Pretty sweet, eh?
Disclaimers
I'm not a Javascript coder. By trade, I'm a non-script Java coder. I've heard of React, and Reactive code, but I don't know what current frameworks are or why they are useful. I've heard there's some neat things about them, like I can declaratively say "This field must match value X", and that's pretty sweet.
What I really wanted to do was reflect changes in one area of my app, a text box, with a nicely rendered view of the same text, say the description of a monster. Wiring things together in Javascript was getting tedious and error-prone. I wanted to make the process more like React and Angular, but I didn't want all the frameworks, and I didn't want to do the research for something which is about 4k of JS or less.
How do I use it?
There's two ways of using bbq. You can use data attributes to wire fields together, or bind your own listeners in javascript. I think the HTML one is more ergonomic.
How do I use it in HTML?
<input type="text" data-bbq-bind-value="bbq-text" ></input>
<p data-bbq-bind-text="bbq-text"></p>
There are four supported bindings:
data-bbq-bind-value
binds the element.value of a field, generally useful for binding textInput fields.data-bbq-bind-text
binds the element.innerText of a field, generally useful for some inline fields.data-bbq-bindattr-${attr}
binds an element's attribute to the variable name.data-bbq-bindprop-${property}
binds an arbitrary JS property to the variable name.
The value of the data-bbq-bind...
attribute is the variable to bind to.
Link the library and call bbq.onLoad()
<head>
<script src="bbq.js"></script>
<script type="text/javascript">
function onLoad() {
console.log("onLoad")
bbq.onLoad();
}
</script>
</head>
<body onload="onLoad()">
How do I use it in JS?
Examine the bbq
class, there are four functions which match the 1:1 bindings listed above.
Bindings for elements
bbq.bindValue(element, variable);
binds the element.value of a field, generally useful for binding textInput fields.bbq.bindText(element, variable);
binds the element.innerText of a field, generally useful for some inline fields.bbq.bindAttr(element, variable, targetAttr);
binds an element's attribute to the variable name.bbq.bindProperty(element, variable, property);
binds an arbitrary JS property to the variable name.
Arbitrary Code Binding
Of course, you can implement your own binder and use it for non-DOM elements
bbq.addBinding(variable, owner, binder)
variable
is a string used to identify the variable in use.owner
is an object used for keeping track of the lifecycle of the binders, with HTML binding it's the element receiving events.binder
is a binding function, it implementsset
andget
. It might also be necessary to implement aaddEventListener(str, callback, options)
method on the binder, but I haven't tried.
- Removal is
bbq.removeBinding(variable, owner)
What's in a name?
I was hungry, and since everything goes through an event queue (My original idea was to implement my own event queue, prior to discovering getTimeout), it seemed appropriate.
How does it work?
As I understand a reactive system, there are several major component to them.
Global Variable Map
Reactive systems generally work on a big global variable map. This map holds a couple things:
- "Variables" and values for the variables to hold.
- Callbacks for Listeners to await changes.
The variable map is forbidden to access directly, instead you need to ensure that listeners are updated when a value is updated. Thus, in barbecue there's some amount of boxing around this raw variable value, so that things get callbacks at the right time.
What about concurrency?
An astute reader or a self-important blog writer might realize there's a problem; if your callbacks mutate the value of the variables, they could thrash or you could be left inconsistent.
Suppose we do the following:
const binder = {
set: function(value) {
console.log('set', value, bbq.variables['example'].value)
}
}
bbq.variables['example'].update('a')
console.log('update', bbq.variables['example'].value)
bbq.variables['example'].update('b')
console.log('update', bbq.variables['example'].value)
What is printed? There are two possible solutions.
Option 1. Write immediately
update a
update b
set a b
set b b
Option 1 has a problem that things can thrash, or be accidentally set out of order. Suppose you're updating a log ID, suddenly the writes to your DB are skipping numbers!
Option 2. Write delayed
update a
update a
set a a
set b b
This has a nice property where things are transactional - The value of a variable at the start of your function is the same as at the end. If needed, you can always read the value from the variables. In a way, the variables are a snapshot at the start of your event, because they go through the same queue that the callbacks do.
Putting the queue in barbecue
I initially envisioned this whole thing with a worker thread, which would read from an event queue and process one of these callbacks at a time - either a write or a read. Then the callbacks would be issued.
What I didn't know is that browsers already have one, with setTimeout! I just issue a new event and it gets queued behind the others. Since these events are generally tiny UI updates, I'm unconcerned about hosing framerates with slow updates.
Thanks
Thanks for reading. My plan for next time is to bring back and talk about Monster Vat, helping us all build creatures.