Avoid circular updates with bind?
In this AQI-related notebook, I had to do some interesting things with Inputs.bind. Specifically:
- I wanted to apply an invertible transform to convert between units (PM2.5 and AQI).
- I wanted to break a circular update to avoid a loss of precision with rounding.
For the latter, I used event.isTrusted: only trusted events propagate from the target to the source. This way when the user interacts with the target (the AQI input), it propagates to the source (the PM2.5 input), but the source doesn’t then propagate back to the target (which would override the original input and lose precision due to roundtrip rounding).
I’m tempted to make this change to Inputs.bind, but I’m guessing it might break some other programmatic usage of Inputs.bind. It might be possible to allow some untrusted events to propagate, but to ignore specifically bind’s own synthetic input event.
function bind(target, source, {
invalidation = Inputs.disposal(target),
transform = d => d,
invert = d => d
} = {}) {
const onsource = (event) => {
if (!event.isTrusted) return;
target.value = transform(source.value);
};
const ontarget = (event) => {
if (!event.isTrusted) return;
source.value = invert(target.value);
source.dispatchEvent(new Event("input", {bubbles: true}));
};
onsource({});
target.addEventListener("input", ontarget);
source.addEventListener("input", onsource);
invalidation.then(() => source.removeEventListener("input", onsource));
return target;
}
I had a similar issue with shared view https://observablehq.com/@tomlarkworthy/shareview which also used trusted events to loop break. BUT, then I start getting error reports that share view does not really work with things like the dataeditor because those generate synthetic events.
For data editor, I wish to emit an input event when the elements are reordered by mouse drag. The mouse is emitting real trusted events, but I need to turn them into "input" events to satisfy Observable programming model. So I am forced to make the conversion that strips the trusted flag.
So I feel like isTrusted is not a very good fit for Observable because custom UI elements often are not emitting pure native events.
How about a flag?
function bind(target, source, {
invalidation = Inputs.disposal(target),
transform = d => d,
invert = d => d
} = {}) {
let dispatching = false;
const onsource = () => {
if (dispatching) return;
target.value = transform(source.value);
};
const ontarget = () => {
if (dispatching) return;
source.value = invert(target.value);
try {
dispatching = true;
source.dispatchEvent(new Event("input", {bubbles: true}));
} finally {
dispatching = false;
}
};
onsource();
target.addEventListener("input", ontarget);
source.addEventListener("input", onsource);
invalidation.then(() => source.removeEventListener("input", onsource));
return target;
}
maybe but it feels like a loaded gun so I am fearful of it.
Philosophically I think 2 way binding is a design mistake and one way binding is the true path. You can create two way binding from composing two one way binds in a yin-yang configuration. Then it's up to the programmer not to shoot themselves in the foot, and if they do, they probably immediately realize it. They also can apply some filtering on one direction to deal with loop breaking but subject to their particular problem at hand.
With a one way bind you can transform at the same time with a simple API, whereas with a two way bind you need to supply a invertible transform which is sometimes impossible (e.g. int -> bool) and not always even relevant for your use case and it feels weird to not supply it though.
oneway bind + transform + filter is the more general purpose building block I feel.
But if you do want 2 way binding, I think you can fix your AQI by having an independant source of truth (Inputs.input), and binding your two sliders off that. The independant source of truth has BOTH units of measurement, and the transform just need to update the "other" quantity. You can use the existing bind propagation rule that source => target does not raise an event so there is no self triggering loop risk.
From https://observablehq.com/@tomlarkworthy/ui-development#2-way-binding-for-synchronization I drew this diagram for 2 way binding that helps me figure out how to arrange these more complex cases. The sliders both need to be under some other authority. There can only be one authority with Inputs.bind

Your plot would then have to be driven of the independant source of truth rather than the slider values (they won't be emitting events), but it doesn't matter it will have all the info anyway.
For the sake of "show me the code" https://observablehq.com/@tomlarkworthy/aqi_no_loop_breaking with two way binding but no loop breaking stratergy needed nor loss of precision.
But still philosophically I believe one-way-binding is the more fundamental approach exactly because not all transforms are invertible and few (most?) cases only require one way binding. Oh another reason is sometimes you want an async transform function (e.g. calling an API to populate a drop down)