Consuming a Custom Web Element in Angular: Issues in DOM Updates

I have written a Typescript-based custom web element which gets data from an input property, and uses it to draw SVG elements.

Essentially, the component has an svg element with some preset children in its template, and in its constructor it gets a reference to this svg element. Whenever it gets data from its snapshot property, it calls a render() function, which uses these data to build more SVG elements and add them as children of either g#base or g#operations:

// custom web element

const template = document.createElement("template");
template.innerHTML = `
<svg id="snapshot">
  <g id="base"></g>
  <g id="operations"></g>
</svg>
`;

export class SnapshotViewComponent extends HTMLElement {
  private readonly _svg: SVGSVGElement;
  private _snapshot?: Snapshot;

  public get snapshot(): Snapshot | undefined {
    return this._snapshot;
  }
  public set snapshot(value: Snapshot | undefined | null) {
    if (this._snapshot === value) {
      return;
    }
    this._snapshot = value || undefined;
    this.render();
  }

  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.shadowRoot?.appendChild(template.content.cloneNode(true));
    this._svg = this.shadowRoot?.querySelector<SVGSVGElement>("#snapshot")!;
  }

  // ...
}

// register the custom element
customElements.define("gve-snapshot-view", SnapshotViewComponent);

Among the SVG elements, some are positioned one next to the other, using getBBox() to measure them for this purpose. The component creates and measures SVG elements like in this code snippet (in a loop):

// custom web element logic

// create a text element for the character appending it to the g element
const text = document.createElementNS(SVG_NS, "text") as SVGTextElement;
text.textContent = char.data;
text.setAttribute("id", id);
text.setAttribute("x", x.toString());
text.setAttribute("y", y.toString());
text.setAttribute("dominant-baseline", "hanging");
g.appendChild(text);

// add style and class features to the text element
const { cstyle, cclass } = this.addStyleAndClassFromFeatures(
  text,
  char.features
);

// measure the bounding box of the text element
const bbox = text.getBBox();

// use bbox to place characters one after another...

This works perfectly in a vanilla HTML page like this:

<!-- sample HTML host page -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- import the web element -->
    <script type="module" src="./dist/index.js"></script>
    <!-- import the consumer code -->
    <script type="module" src="consumer.js"></script>
  </head>
  <body>
  <!-- the custom web element -->
  <gve-snapshot-view></gve-snapshot-view>
  </body>
</html>

The consumer JS code sets data and I can see them displayed as expected:

// consumer code for the element in HTML

document.addEventListener("DOMContentLoaded", (event) => {
  const snapshot = { /* ... data ... */ };
  let component = document.querySelector("gve-snapshot-view");
  if (!component) {
    console.error("Component not found");
  } else {
    component.snapshot = snapshot;
  }
});

I am then packing this component with NPM and importing it into an Angular 18 application. There, an Angular component is in charge of letting users edit data modeled as a Snapshot; the web element is bound to edited data, and whenever this edited data changes, it must render its SVG. So:

  1. I install the NPM packaged component in the Angular app.
  2. I add schemas: [CUSTOM_ELEMENTS_SCHEMA] to the component’s schemas (this is a module-less Angular 18 app) to allow for custom elements in its template.
  3. I add in its template the custom web element selector with data binding:
<gve-snapshot-view [snapshot]="snapshot" />

where snapshot is a public member of the Angular component, and gets replaced with a new object instance whenever there is an update.

Now I can see from the log that the web element receives the data (snapshot in code) and builds the SVG elements as expected; BUT the problem is that whenever it calls getBBox() on a built element, like in the above example for text, this returns a 0 size. This causes all the SVG elements to be displayed one on top of the other.

This code works in the vanilla HTML page, but not in Angular. So I assume that Angular or zone are interfering with the web element interactions with the DOM. Probably the SVG elements added to the web element’s DOM are not yet rendered when they are measured with getBBox(). This does not happen in the test HTML page, but it happens in Angular, maybe because of zone. As a quick fix attempt, I tried to wrap the update of snapshot in the host Angular component within a runOutsideAngular function, like this:

// Angular host component

@Input()
public get snapshot(): Snapshot | undefined {
  return this._snapshot;
}
public set snapshot(value: Snapshot | undefined | null) {
  if (this._snapshot === value) {
    return;
  }
  // set data outside zone
  this._zone.runOutsideAngular(() => {
    this._snapshot = value || undefined;
  });
  this.updateForm(this._snapshot);
}

Now, whenever I update data, I just do this.snapshot = newData; to trigger this. Anyway, nothing changed, and I still get 0-sized bounding boxes.

So I wonder about the best strategy to work around this. The reason for using a vanilla web element outside of Angular is right to have a lighter component which just deals with the DOM in its own template, which should be a black box for the Angular host. Not only this would be much more complex in Angular, where touching the DOM is usually not the way to go; but I have also the requirement of providing this rendition logic as a custom web element, because other clients will be consuming it (e.g. vanilla JS, Blazor, React, etc.). So, having a custom web element allows me to keep all the DOM manipulations inside its black box, and reuse it in many different frameworks.

On the Angular side, I would like a solution which does not burdens the web element with complex hacks just to let it work seamlessly in that environment. For instance:

  • I know of SVG helper libraries like svg.js, but this being an Angular-related timing issue I am not sure whether it will be of help in this case, nor I want to add a third-party dependency just to create SVG elements unless I am forced to.

  • if this is a timing issue, and I have somehow to delay the measurement of newly created SVG elements to a time when they are effectively rendered, this would require refactoring all the inner logic of the component; but even in this case, how can I be sure about the right time for measuring SVG elements? I can think of placing the logic inside a requestAnimationFrame, which should delay what’s inside it until the next frame, when hopefully the SVG elements will be rendered; or using MutationObserver on the children of the web element’s svg element. In any case, this would require the component logic to be split: no longer a single loop which adds character by character as text SVG elements, but first adding them, then wait somehow, and then move them. In turn, this also implies some trick to avoid flickering or letting users see a messed up text before its characters are moved to the right positions, like e.g. setting their initial opacity to 0, and then restoring it back later when they are positioned.

Of course, none of these approaches is ideal, but before trying any I am open to suggestions if anyone has experience in consuming (not building) custom web elements in Angular.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật