An Angular Elements feedback widget in a CMS page misses the 'ready' output it emits in ngOnInit and loses its inputs after the CMS detaches and re-attaches it; why?
answer
- who existed first, event or listener
- connect creates the component synchronously
- disconnect waits on a short timer
- reconnect builds a fresh instance
- cached inputs are applied once
basics
~20 sConnecting the element creates the component and runs ngOnInit synchronously during insertion, so a listener added afterwards misses 'ready'. Removal destroys the component after a short timer; reconnecting later builds a fresh instance and does not replay earlier inputs.
solid answer
~40 sIn `@angular/elements`, the element's `connectedCallback` creates the component, applies cached inputs, subscribes to outputs and runs the first change detection - all synchronously inside the `appendChild` or `innerHTML` that inserted it. An output emitted in `ngOnInit` is dispatched right then, so a listener the CMS adds after insertion never sees it. Attach listeners to the node before inserting it, or expose readiness as state rather than a one-shot event. On removal, `disconnectedCallback` schedules `componentRef.destroy()` on a short timer (10 ms in the current source), so a quick move keeps the instance. If the node comes back later, a new component is created, but the values set earlier were consumed when first applied and unchanged attributes fire no callback, so inputs start from their defaults. The guide lists this re-attach behaviour as a known limitation.
code
ts · 13 linesconst widget = document.createElement('kj-feedback') as HTMLElement & {
productId: string;
};
widget.productId = 'sku-42';
// Listen before inserting: ngOnInit runs during appendChild.
widget.addEventListener('ready', () => console.log('widget ready'));
document.querySelector('#feedback-slot')!.appendChild(widget);
function reattach(slot: Element) {
slot.appendChild(widget);
// A new component may have been created: re-apply its inputs.
widget.productId = 'sku-42';
}go deeper
Know that an Angular Elements component is created when the element is connected, and destroyed some time after it is removed.
Walk through connectedCallback: cached inputs applied, outputs subscribed, first change detection run, all synchronously during insertion.
Diagnose host-page ordering bugs and re-attach state loss, and prefer state on the host over one-shot events for late subscribers.
Write down the embedding contract for CMS teams: when to attach listeners, avoiding detach and re-attach, and which state the widget restores itself.
## The scenario A feedback widget is built as an Angular component, packaged with `createCustomElement()` and registered as `kj-feedback`. A CMS inserts it into article pages. Two bug reports arrive: 1. The CMS's analytics code listens for the widget's `ready` output, which the component emits in `ngOnInit`, and never receives it. 2. The CMS has a "preview" mode that detaches a page region and re-attaches the same DOM nodes later. After that the widget renders with default settings: the product id and labels the CMS set are gone. Both come from how `@angular/elements` maps the **custom element lifecycle** onto the **component lifecycle**. ## When the component is created An Angular Elements element holds a *strategy* object. Nothing Angular-side exists until the element is **connected**: - Before connection, attribute changes and property sets are **cached** as pending input values. - In `connectedCallback`, the default strategy creates the component with `createComponent(..., { hostElement: element })`, applies the cached inputs, subscribes to every output, attaches the host view to the `ApplicationRef`, and calls `detectChanges()` on it. - That first `detectChanges()` runs `ngOnInit` (and the rest of the first pass). The browser runs `connectedCallback` for a defined element **synchronously during the insertion** - inside `appendChild`, `insertBefore` or the `innerHTML` assignment. Angular subscribes the element to its outputs *before* the first change detection precisely so that events emitted during initialisation are dispatched. But dispatching a DOM event only reaches listeners that already exist. | Page code order | Does the listener see `ready`? | |---|---| | create node, `addEventListener('ready')`, then `appendChild` | Yes | | `appendChild`, then `addEventListener('ready')` | No - dispatched during `appendChild` | | markup inserted via `innerHTML`, then `querySelector` + listener | No - dispatched during the assignment | | markup present, listener attached, widget script loads later | Yes - the component is created when the element is upgraded | The event is also created without `bubbles`, so a listener on a container never catches it regardless of timing. **Fixes:** - Attach listeners to the node before inserting it, or before the widget bundle defines the tag. - Prefer **state over one-shot events** for anything a late subscriber needs: reflect readiness in an attribute or class on the host (for example through a host binding) that the CMS can read at any time. - Keep `ready` for consumers that are guaranteed to be in place first. ## What removal does `disconnectedCallback` does not destroy immediately. The strategy schedules `componentRef.destroy()` with `setTimeout` - a delay of **10 ms** in the v22 source - and `connectedCallback` cancels that timer if the node comes back first. This is what lets code *move* an element (remove and re-insert in the same task) without tearing it down. The element stops forwarding output events as soon as it is disconnected (and resubscribes on reconnect). If the node stays detached longer than the delay, the component is destroyed: `ngOnDestroy` and `DestroyRef` callbacks run. ## Why re-attaching loses the inputs When the same node is connected again, the strategy has no component, so it creates a **new** one. What it applies to that new instance is its cache of pending inputs - and that cache was cleared when the first instance consumed it: 1. The first connect applied the cached attribute and property values, then cleared the cache. 2. Later property sets went straight to the first component through `setInput`. 3. The first component was destroyed; its values went with it. 4. On reconnect, the cache is empty, and attributes that are still on the node fire no `attributeChangedCallback` because they did not change. So the fresh component starts from its input defaults. The Angular guide's **Limitations** section flags destroying and re-attaching Angular Elements elements as needing care for this reason. **Fixes, from least to most invasive:** - Have the CMS **hide** the region instead of detaching it. - After re-attaching, **re-set** the properties (and re-write any attributes) the widget needs. - Replace the node with a **new element**: a freshly upgraded element reads every attribute present on it. - Keep durable state outside the component (a service in the shared application injector, or the server) so a fresh instance can restore it. ## How to reproduce it in a test page Insert the element, set an input property, detach it, wait longer than the destroy delay, re-insert the same node, then read the property: it returns the input's default, and a `DestroyRef.onDestroy` callback registered in the component will have run once.
- Why does moving an Angular Elements node from one container to another keep the same component instance?A move is a removal followed by an insertion. `disconnectedCallback` only schedules `destroy()` on a short timer, and the following `connectedCallback` cancels that timer, so the existing `ComponentRef` and its state survive the move.
- If the widget's markup is already in the page and its bundle loads later as a module script, when does ngOnInit run?When `customElements.define` upgrades the existing node: the attributes already present are delivered, and because the node is connected, the component is created and initialised then. Listeners attached to the unupgraded node earlier are kept, because it is the same DOM node.
A cloakroom that waits a moment before clearing a hook: step out and straight back and your coat is still there, but return an hour later and you get an empty hook, not your old coat.
saying these in an interview costs you the question
- Angular queues output events until a listener subscribes
- Removing the element destroys its component synchronously
- Re-attaching the same node replays every earlier input
- ngOnInit waits for the next animation frame after insertion
- Output events bubble, so a container listener catches ready