Summary
When a webui-framework parent is materialized client-side, each child element is appended to the DOM (firing its connectedCallback) before the parent pushes its bound attributes/properties. The parent's $patchAttr pass then writes the bound value onto the child, overwriting anything the child set in its own connectedCallback.
The net effect: a child cannot self-resolve a value from a browser/runtime API in connectedCallback and also be the target of any parent :prop binding — whatever the child set is wiped out by the parent's subsequent write.
Minimal repro
Three source files; one command; open a browser.
child/my-child.html — child displays its current value:
<div>child.value = "{{value}}"</div>
parent/my-parent.html — parent binds its (undefined) val to the child:
<my-child :value="{{val}}"></my-child>
index.html — defines both classes, mounts <my-parent> client-side. The child's connectedCallback tries to resolve value itself:
<!DOCTYPE html>
<html><body style="font:14px sans-serif;padding:20px">
<!-- A hidden SSR instance just so `webui serve` registers the templates. -->
<my-parent style="display:none"></my-parent>
<h2>Expected</h2>
<div style="border:1px dashed #888;padding:8px">child.value = "set-by-child"</div>
<h2>Actual (CSR-mounted my-parent)</h2>
<div style="border:1px dashed #888;padding:8px" id="mount"></div>
<script type="module">
import { WebUIElement, observable } from "https://esm.sh/@microsoft/webui-framework";
class MyChild extends WebUIElement {
connectedCallback() {
super.connectedCallback();
// Resolve value from a "browser API" (here: a literal).
this.value = "set-by-child";
}
}
observable(MyChild.prototype, "value");
MyChild.define("my-child");
class MyParent extends WebUIElement {}
// Parent declares the bound prop. (Whether it has a value or not
// doesn't matter — see notes below.)
observable(MyParent.prototype, "val");
MyParent.define("my-parent");
document.getElementById("mount").appendChild(document.createElement("my-parent"));
</script>
</body></html>
Serve and open in any modern browser:
npx webui serve <repro-dir> --port 3000 --plugin webui
# → open http://localhost:3000/
What you see
- Expected —
child.value = "set-by-child". The child's connectedCallback ran and assigned a value; nothing else writes to value afterwards.
- Actual —
child.value = "". The child's assignment is overwritten by the parent's attribute-binding pass, which runs after the child's connectedCallback. (The overwriting value happens to be empty here because the parent never set val; in the general case whatever the parent's binding evaluates to wins, regardless of what the child did in its callback.)
The bound prop and the child's self-resolved value cannot coexist. Every other web-component framework in common use (Lit, FAST, React, Vue, Stencil) applies bound props before invoking connectedCallback / mount lifecycle, so the child sees the real value in its callback and any assignment it makes is final.
Summary
When a
webui-frameworkparent is materialized client-side, each child element is appended to the DOM (firing itsconnectedCallback) before the parent pushes its bound attributes/properties. The parent's$patchAttrpass then writes the bound value onto the child, overwriting anything the child set in its ownconnectedCallback.The net effect: a child cannot self-resolve a value from a browser/runtime API in
connectedCallbackand also be the target of any parent:propbinding — whatever the child set is wiped out by the parent's subsequent write.Minimal repro
Three source files; one command; open a browser.
child/my-child.html— child displays its currentvalue:parent/my-parent.html— parent binds its (undefined)valto the child:index.html— defines both classes, mounts<my-parent>client-side. The child'sconnectedCallbacktries to resolvevalueitself:Serve and open in any modern browser:
What you see
child.value = "set-by-child". The child'sconnectedCallbackran and assigned a value; nothing else writes tovalueafterwards.child.value = "". The child's assignment is overwritten by the parent's attribute-binding pass, which runs after the child'sconnectedCallback. (The overwriting value happens to be empty here because the parent never setval; in the general case whatever the parent's binding evaluates to wins, regardless of what the child did in its callback.)The bound prop and the child's self-resolved value cannot coexist. Every other web-component framework in common use (Lit, FAST, React, Vue, Stencil) applies bound props before invoking
connectedCallback/ mount lifecycle, so the child sees the real value in its callback and any assignment it makes is final.