Skip to main content
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Doug Hughes and Ezra Parker and Dan Wilson and John Mason and Jason Dean and Luis Majano and Mark Mandel and Brian Kotek and Wil Genovese and Rob Brooks-Bilson and Andy Matthews and Simeon Bateman and Ray Camden and Chris Rockett and Joe Bernard and Dan Skaggs and Byron Raines and Barney Boisvert and Simon Free and Steve 'Cutter' Blades and Seth Bienek and Katie Bienek and Jeff Coughlin
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Doug Hughes Ezra Parker Dan Wilson John Mason Jason Dean Luis Majano Mark Mandel Brian Kotek Wil Genovese Rob Brooks-Bilson Andy Matthews Simeon Bateman Ray Camden Chris Rockett Joe Bernard Dan Skaggs Byron Raines Barney Boisvert Simon Free Steve 'Cutter' Blades Seth Bienek Katie Bienek Jeff Coughlin

Trying To Implement 9-Slice Scaling With SVG Components In Angular 7.2.4

By
Published in

I'm very new to SVG (Scalable Vector Graphics). I've read Practical SVG by Chris Coyier. And, I've tried to create an SVG icon system using components in Angular 7.2.0. But, really, I know very little about how SVG works or about how to make it dynamic. Up until now, everything that I've done with SVG has been static. And, one thing that I've been curious about recently is how to scale SVG graphics dynamically. Specifically, how - or even if - I can implement 9-Slice scaling with SVG graphics in Angular. From what I've been able to find on Google, there is no native way to do 9-Slice scaling with SVG. So, I tried to implement it using Math in Angular 7.2.4.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

The idea behind 9-Slice scaling is that you take an object and you split it up into 9-slices in which each slice defines how the given object should scale when it is resized. So, for example, if we had a rectangle with rounded corners, we'd want to slice it up so that the corners remain at a fixed size while the width and the height of the rectangle can be stretched:

9-Slice scaling create static and dynamic sections of an object.

Slicing the rectangle up in this manner allows the "detail rich" parts (ie, the rounded corners) to remain fixed while allowing the "detail poor" parts to be stretched and distorted. Of course, since the stretchy parts don't really have any detail, the distortion caused by the stretching doesn't result in any funky artifacts.

Now, again, I'm super new to SVG; but, from what I've been reading, this kind of scaling doesn't appear to be part of the SVG semantics. You can use the "preserveAspectRatio" attribute to influence the way an SVG element scales inside of its viewBox. And, Sara Soueidan has a fascinating article on dynamic SVG layouts using nested SVG elements. But, ultimately, SVG coordinates appear to be "absolute".

Eventually, I came across the post, Dynamic SVG Components by Dave Geddes. In his post, Geddes creates an SVG component that, more or less, uses the concept of 9-slice scaling to create a custom container. His post was awesome - and is very much the inspiration for my post; but, his post also showed me that there was no easy answer - that the scaling has to be done using good old-fashion Math.

So, to try and bring Geddes' approach into my own mental model, I wanted to see if I could create a Pop-Up component in Angular 7.2.4 that would scale dynamically based on its projected content. And, to make the experiment even more exciting, the pop-up container has a little bottom carrot. So, not only do I have to keep the corners of the pop-up "fixed", I also have to keep the carrot's size and position fixed to the center of the bottom length.

First, let's look at how this component will be consumed. In my root component, I am using several instances of the "pop-up" component, each with a different amount of content:

// Import the core angular services.
import { Component } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "my-app",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<my-popup>
			Hello world!
		</my-popup>

		<my-popup>
			Hello world! What a wonderful day to be alive!
		</my-popup>

		<my-popup>
			Peace has cost you your strength.<br />
			Victory has defeated you.<br />

			<div class="author">
				- <strong>Bane</strong>
			</div>
		</my-popup>

		<my-popup style="width: 600px ;">
			We are all meant to shine, As children do. We were born to make manifest
			The glory of God that is within us. It's not just in some of us; It's in
			everyone. And as we let our own light shine, We unconsciously give other
			people permission to do the same. As we're liberated from our own fear,
			Our presence automatically liberates others.

			<div class="author">
				- <strong>Marianne Williamson</strong>
			</div>
		</my-popup>
	`
})
export class AppComponent {
	// ...
}

As you can see, we have different kinds of content and different kinds of constraints. Some of the content is free-form. Some has line breaks. And, in the last instance, the width of the pop-up is a fixed-size, allowing the pop-up component to scale in the vertical direction only.

My strategy here is to render the SVG Element using absolute position inside of the pop-up component:

:host {
	display: inline-block ;
	overflow: hidden ;
	position: relative ;
}

.svg {
	bottom: 0px ;
	left: 0px ;
	position: absolute ;
	right: 0px ;
	top: 0px ;
}

As you can see, the SVG element will stretch to cover the pop-up host. On its own, this layout would do nothing but stretch and distort the embedded SVG paths. So, in order to ensure proper scaling, I'm going to measure the dimensions of the host element and then use those dimensions to set the SVG viewBox and the coordinates of the embedded SVG path.

This part doesn't include any magic. I designed my pop-up in InVision Studio and I exported its SVG. Then, I went about converting the absolute path coordinates into dynamic math-based coordinates. This was a grueling process and took me over 3-hours. Especially since I had a lot of trouble converting the rounded-corner "curves" to use "relative" coordinates (c), not absolute coordinates (C).

Ultimately, here's the Angular component that I ended up with:

// Import the core angular services.
import { AfterViewInit } from "@angular/core";
import { Component } from "@angular/core";
import { ElementRef } from "@angular/core";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
	selector: "my-popup",
	styleUrls: [ "./popup.component.less" ],
	template:
	`
		<svg class="svg" style="display: none ;">
			<defs>
				<filter id="shadow">
					<feDropShadow
						dx="0"
						dy="2"
						stdDeviation="1"
						flood-color="#000000"
						flood-opacity="0.4"
					/>
				</filter>
			</defs>

			<path
				fill="#fafafa"
				stroke="#666666"
				stroke-width="1"
				vector-effect="non-scaling-stroke"
				style="filter: url( #shadow ) ;"
			/>
		</svg>

		<div class="content">
			<ng-content></ng-content>
		</div>
	`
})
export class PopupComponent implements AfterViewInit {

	private elementRef: ElementRef;

	// I initialize the pop-up component.
	constructor( elementRef: ElementRef ) {

		this.elementRef = elementRef;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I get called after the view and content have been initialized.
	public ngAfterViewInit() : void {

		// NOTE: Since we are updating the VIEW settings based on the DIMENSIONS OF THE
		// RENDERED VIEW, we are going to set the element attributes explicitly (not
		// declaratively using template bindings). If we try to use template bindings,
		// we'll get errors about values being changed after they were already checked.
		// As such, we'll just skip the bindings and just DO IT LIVE!

		var element = this.elementRef.nativeElement;
		var svg = element.querySelector( "svg" );
		var path = svg.querySelector( "path" );

		// We are going to scale the SVG based on the dimensions of the host element.
		// This way, we can have the SVG scale relative to the projected content.
		var rect = element.getBoundingClientRect();

		// We can't just fit the SVG PATH to the bounds of the host element. If we do
		// this, we'll end up clipping the drop-shadow and parts of the border. As such,
		// we have to make the dimensions of the path SMALLER than the viewBox.
		// --
		// CAUTION: These numbers were just trial-and-error until stuff stopped getting
		// clipped. I am not sure why the viewBox needs more room on the right???
		var width = ( rect.width - 8 );
		var height = ( rect.height - 8 );
		var halfWidth = ( width / 2 );

		// Again, we need to offset the viewBox so that we don't clip the path and its
		// drop shadow.
		var svgViewBox = `-3 -3 ${ width + 5 } ${ height + 5 }`;

		// In order to size the SVG using 9-slice scaling, we have to calculate the path
		// points based on the available space. As such, we're going to use template
		// strings with interpolated maths.
		// --
		// NOTE: In order to make things a little easier, the CURVE segments (c) have
		// been converted to use "relative" values. This makes them independent of the
		// size of the overall path.
		var svgPath = [
			`M 0,7`,
			// Top-left corner.
			`c 0,-3.8634033203125 3.1365966796875,-7 7,-7`,
			`L ${ width - 7 },0`,
			// Top-right corner.
			`c 3.1365966796875,0 7,3.1365966796875 7,7`,
			`L ${ width },${ height - 7 - 12 }`,
			// Bottom-right corner.
			`c 0,3.8634033203125 -3.1365966796875,7 -7,7`,
			// Carrot-start.
			`L ${ halfWidth + 7 },${ height - 12 }`,
			`L ${ halfWidth },${ height }`,
			`L ${ halfWidth - 7 },${ height - 12 }`,
			// Carrot-end.
			`L 7,${ height - 12 }`,
			// Bottom-left corner.
			`c -3.8634033203125,0 -7,-3.1365966796875 -7,-7`,
			`L 0,7`,
			`Z`
		];

		// Update the SVG properties and render the element in the view.
		svg.setAttribute( "viewBox", svgViewBox );
		path.setAttribute( "d", svgPath.join( "\n" ) );
		svg.style.display = "block";

	}

}

As you can see, this is just a lot of math. I get the dimensions of the host element and then I use those dimensions to manually implement the 9-slice scaling. It was a pain in the butt. That said, if we render this demo, we can see that it actually kind of works:

Tryign to implement 9-Slice scaling in a pop-up component in Angular 7.2.4.

This appears to do what I want it to do. But, keep in mind that this is just an experiment. I am not sure if this is an approach that you'd want to take, especially since I have to define the SVG based on the rendered dimensions of the host element. This, in and of itself, poses a number of problems. Consider the fact that I am using the ElementRef.nativeElement to define the SVG attributes. I am doing this - instead of using a declarative template-based approach - because I can't have cascading view-updates without adding some sort of janky timeout (at least not as far as I know).

It would be awesome if 9-Slice scaling was somehow part of the SVG paradigm. But, it's cool to know that I can manually fill-in the feature-gap if absolutely necessary. If nothing else, this experiment reminds me that SVG is really an underutilized resource in my own Angular applications.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel