LiveLoveApp logo

Responsive SVGs in Angular with ResizeObserver, Signals, and Observables

Published by Mike Ryan on
Responsive SVGs in Angular with ResizeObserver, Signals, and Observables
Responsive SVGs in Angular with ResizeObserver, Signals, and Observables

In today’s post, we’ll explore how to use Angular's ResizeObserver, signals, and observables to create SVG elements that respond to container size changes automatically. This setup forms the foundation for responive charts in Highpass, our upcoming Angular-native chart library. Highpass is still in development, but if you want to follow along with our progress, keep checking back here on the LiveLoveApp blog or follow me on X (Twitter) at @MikeRyanDev.

Step 1: Creating a Basic Angular Component for SVG Wrapping

Our starting point is a minimal Angular component that wraps a single SVG element. By leveraging the dynamic sizing of the host element, we can project its dimensions directly onto the SVG. This approach allows the SVG to scale as its container changes size without manual adjustments.

Here’s what our base component looks like:

import { Component, ElementRef, inject, ViewChild } from '@angular/core';

@Component({
  selector: 'responsive-svg',
  template: `<svg #target></svg>`,
})
export class ResponsiveSvgComponent {
  hostElementRef = inject(ElementRef);
  svgElementRef: viewChild<ElementRef<SVGElement>>('target')

  // Other responsive logic will follow
}

This is a straightforward setup: the component template contains an SVG element wrapped in a container. We’ll observe the size of this host container element and project that size directly onto the SVG element, making the SVG’s width and height responsive to its container.

Step 2: Setting Up a Size Observable with ResizeObserver

Next, we need to track changes to the component’s size. By injecting ElementRef, we can access the host element’s dimensions and monitor them with ResizeObserver. Here’s how to set up an observable chartSize$ that emits the host element’s size whenever it changes:

chartSize$ = new Observable<Rectangle>((subscriber) => {
  const el = this.hostElementRef.nativeElement;
  const rect = el.getBoundingClientRect();
  subscriber.next({
    start: [0, 0],
    end: [rect.width, rect.height],
  });

  const resizeObserver = new ResizeObserver(() => {
    const rect = el.getBoundingClientRect();
    subscriber.next({
      start: [0, 0],
      end: [rect.width, rect.height],
    });
  });
  resizeObserver.observe(el);

  return () => {
    resizeObserver.disconnect();
  };
});

Explanation:

  • Observable with ResizeObserver: This observable watches for size changes in the host element and emits a Rectangle whenever the size changes.
  • Cleanup: We return a disconnect function to stop observing when the component is destroyed, preventing memory leaks.

Step 3: Converting the Observable to a Signal

With the size observable in place, we convert it into a signal using toSignal. This lets us access the component’s dimensions reactively throughout the component:

rect = toSignal(this.chartSize$, {
  requireSync: true,
});

In the constructor of our chartSize$ observable, we syncrhonously emitted the host element's initial rectangle size. That let's us set the requireSync flag to true in toSignal(...). The advantage of this approach is that, without requireSync (or supplying an initialValue), the type of our signal would be Signal<Rectangle | undefined>. With the synchronous requirement, instead the type is just Signal<Rectangle>, letting us avoid undefined checks throughout the implementation.

This signal, rect, now updates automatically whenever the container changes size, so we can use it in any calculation or layout adjustment in our component.

Step 4: Applying Dynamic Dimensions to the SVG

With Angular’s effect, we set the SVG’s width and height attributes based on rect. Each time rect updates, effect updates the SVG dimensions automatically.

constructor() {
  effect(() => {
    const el = this.svgElementRef()?.nativeElement;
    if (!el) {
      return;
    }

    const [x, y] = this.rect();

    el.setAttribute('width', `${x}`);
    el.setAttribute('height', `${y}`);
    el.setAttribute('viewBox', `0 0 ${x} ${y}`);
  });
}

The effect observes rect and applies its dimensions to the SVG, making it adapt reactively to size changes in the host element.

Responsive Layouts Using Signals

Now that our SVG scales responsively, we can extend this setup to create responsive layouts. For example, if we define the xScaleRange using signals, it can dynamically adjust based on the size of rect:

xScaleRange = computed(() => {
  const dataRect = this.dataRect();
  return [dataRect.start[0], dataRect.end[0]] as const;
});

Whenever rect changes, xScaleRange recalculates, ensuring the layout adapts to the updated dimensions without manual intervention.

Why This Pattern Works

  1. Efficient Updates: ResizeObserver triggers updates only when necessary, meaning there’s no unnecessary work happening.
  2. Declarative Adjustments: Using observables and signals keeps the layout declarative. We don’t need imperative resize logic, which keeps things simple and clear.
  3. Scalable: This setup works for any SVG component, making it a versatile addition for any responsive visualization needs.

Future Applications in Highpass

As we continue building Highpass, this approach will form the foundation of its responsive layout capabilities, allowing components to adjust dynamically to different screen sizes and layouts. If you’re interested in more techniques like this for Angular and SVG, check back here on the LiveLoveApp blog or follow me on X (Twitter) at @MikeRyanDev.

Bring data to life on the web
Schedule a Call