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 aRectangle
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
- Efficient Updates:
ResizeObserver
triggers updates only when necessary, meaning there’s no unnecessary work happening. - Declarative Adjustments: Using observables and signals keeps the layout declarative. We don’t need imperative resize logic, which keeps things simple and clear.
- 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.