Angular Progress Component with SVG
Often when building an application with visualizations, we reach for a third party library. Many of these libraries are great for display charts and graphs with a rich feature set. Sometimes though we need something lightweight and don't want to pull in another dependency to our Angular app. In this post, we are going to look at creating a progress visualization component and see how easy it is to do this with Angular and SVG.
Our component will look like the example below.
SVG Template and CSS Styles
To create this component, we are going to use SVG to render the circle and then Angular to update the SVG efficiently. First, let's take a look at the SVG.
<svg width:"120" height:"120" viewBox="0 0 120 120" class="progress__svg">
<circle
[attr.r]="radius"
cx="60"
cy="60"
stroke-width="12"
class="progress__meter"
/>
<circle
[style.strokeDasharray]="circumference"
[style.strokeDashoffset]="dashoffset"
[attr.r]="radius"
cx="60"
cy="60"
stroke-width="12"
class="progress__value"
/>
</svg>
Out SVG consists of two circles. One circle will be the outer grey while the other will be the progress value that will be updated by Angular. If we look at our SVG, the first thing we do is bind the circumference value to each circle.
The circumference value will be set in the component class. Notice that we can use Angular's property binding syntax []
to set properties and attributes of our SVG.
<circle
[style.strokeDasharray]="circumference"
[style.strokeDashoffset]="dashoffset"
[attr.r]="radius"
cx="60"
cy="60"
stroke-width="12"
class="progress__value"
/>
Looking at the second circle, we bind two style properties, strokeDasharray
and strokeDashoffset
. Using these style properties, we can calculate the progress value in our component and Angular will automatically update the styles to reflect the changes.
Now we need to write a bit of CSS to style everything correctly on our component. This CSS sets a few style properties on our SVG such as color and fill.
.progress__svg {
transform: rotate(-90deg);
}
.progress__meter,
.progress__value {
fill: none;
}
.progress__meter {
stroke: #ccc;
}
.progress__value {
stroke: #4caf50;
transition: all;
}
Component TypeScript
Now that we have our template and CSS set up we need to write the logic to update our progress meter.
import {
Component,
Input,
OnInit,
OnChanges,
SimpleChanges
} from '@angular/core';
@Component({
selector: 'app-progress',
templateUrl: './progress.component.html',
styleUrls: ['./progress.component.css']
})
export class ProgressComponent implements OnInit, OnChanges {
radius = 54;
circumference = 2 * Math.PI * this.radius;
dashoffset: number;
constructor() {
this.progress(0);
}
ngOnInit() {}
private progress(value: number) {
const progress = value / 100;
this.dashoffset = this.circumference * (1 - progress);
}
}
We will first start will our calculations to render the SVG correctly. In our component, we define the following properties, radius
, circumference
and dashoffset
. Radius is a constant value that our SVG circles bind to for their radius attribute. We then calculate the circumference
to bind to our second circle's style property strokeDasharray
, this creates our circle. We then calculate the strokeDashoffset
which is what creates our green fill effect.
To calculate the strokeDashoffset
, we create a method called progress()
. The progress()
method takes in a value 0 - 100 and then computes the
appropriate offset value to set to the dashoffset
property. In our constructor, we initialize this value to 0. If we looked at our component, currently we wouldn't see any progress value. We ideally want to be able to pass the progress value into this component so that other components can use it efficiently.
Inputs and ngOnChanges
To make this component reusable, we are going to add a @Input
property called value. With this, we can pass in a given value from a parent component and render that progress value.
import {
Component,
Input,
OnInit,
OnChanges,
SimpleChanges
} from '@angular/core';
@Component({
selector: 'app-progress',
templateUrl: './progress.component.html',
styleUrls: ['./progress.component.css']
})
export class ProgressComponent implements OnInit, OnChanges {
@Input() value: number;
radius = 54;
circumference = 2 * Math.PI * this.radius;
dashoffset: number;
constructor() {
this.progress(0);
}
ngOnInit() {}
ngOnChanges(changes: SimpleChanges) {
if (changes.value.currentValue !== changes.value.previousValue) {
this.progress(changes.value.currentValue);
}
}
private progress(value: number) {
const progress = value / 100;
this.dashoffset = this.circumference * (1 - progress);
}
}
Adding the @Input() value
property is not enough. We now need to implement the ngOnChanges
life cycle hook. The ngOnChanges
will notify us whenever an input property changes, allowing us to efficiently trigger when the progress()
method should be called to recalculate our dashoffset
.
Once set up we can now use our component in our template like so:
<app-progress [value]="75"></app-progress>
As we can see Angular's binding syntax allows us to quickly update SVG elements and create fast and efficient svg animations for our apps. Check out the full working demo below!