#26 Animating the Stroke Dashoffset
Thursday, March 29, 2018
In this article we will add a message and svg element to our send screen component. The send screen will display a default message whenever the it is animated into view in response to the user clicking the send button and we will animate the changing of that message once a response is received from the server. We will also animate one or more svg elements along with the updating of the message. For the svg elements we will be animating a property name the stroke dashoffset which will result in it appearing that we are actually drawing them on the screen. When we are done with this article we will be able to respond to an error from the server if the user is attempting to login. In subsequent articles we will deal with the rest of the communication possibilities from the server.
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
You can view a completed example of the authenticator component here.
Misc Starting Changes
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.scss
- send-screen.component.ts
-
authenticator
-
sass
-
2-base
- _functions.scss
-
2-base
-
components
-
Source
We are going to begin this article by making a few miscellaneous changes to a few different files. First off we are going to move our
em
function out of our authenticator's scss file and to a new file that we are going to create located
in our sass base folder (a). Next since we will be dealing with various components and animations
for our send screen if we comment out the overflow: hidden;
statement within our authenticator scss
file it will make things a lot easier (b). Finally I realized in the last article that
I did not follow my own naming convention as it applies to references to DOM elements within our typescript classes. In order to fix
this we need to rename the _container
field to
_sendScreenDOM
and update any reference made to it (c).
_functions.scss (1)
@function em($pixel, $base-font-size: 16px) {
@return #{strip-unit($pixel)/strip-unit($base-font-size)}em;
}
authenticator.component.scss (1)
...
@import "../../sass/2-base/_functions.scss";
...
.authenticator {
...
/*overflow: hidden;*/
...
}
send-screen.component.ts (1)
...
export class SendScreenComponent ... {
...
private _sendScreenDOM: HTMLElement = null; // <- Renamed _container
...
public ngAfterViewInit(): void {
this._sendScreenDOM = this._domReader.findChildById(this._elementRef.nativeElement, this.sendScreenId);
}
...
public hide = () => {
this._sendScreenDOM.animate(...);
}
public show = () => {
this._sendScreenDOM.animate(...);
}
}
For reference once we have removed the overflow hidden statement we should once again be able to see the send screen and it should be positioned above our component (d).
Conveying a message to the user
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.pug
- send-screen.component.scss
- send-screen.component.ts
-
authenticator
-
components
-
Source
The next thing that we are going to work on is being able to present a message to the user through the send screen. We will of course start by modifying our template (e). Here we are just binding the inner html of a div to a property called 'message'. We are also applying a css class to it so that we are able to add a little bit of styling (f). With the template referencing our message property we just need to add it to our typescript class (g). We will also define a default message so that it is easy to set our message equal to it whenever we return our send screen to its hidden position.
send-screen.component.pug (1)
div.send-screen(...)
div.message([innerHtml]="message")
send-screen.component.scss (1)
...
@import "../../sass/2-base/_functions.scss";
...
.send-screen {
...
.message {
@include position(absolute, null 0 0 0);
text-align: center;
color: white;
padding: em(16px);
font-size: em(24px);
}
}
send-screen.component.ts (2)
...
export class SendScreenComponent ... {
private readonly _defaultMessage: string = "One moment please your request is being processed.";
...
private _message: string = null;
public get message() { return this._message; }
...
constructor(...) {
this._message = this._defaultMessage;
}
...
}
With those changes made we should see our message being displayed within our send screen (h). To save space I am just showing the send screen but it is still located above the authenticator ui.
Responding to a failure
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- send-screen.component.ts
-
authenticator
-
components
-
Source
It of course doesn't do us much good to just be able to show the default message so the first method we are going to introduce that will modify the message is a failure method (i). We will also have this method call the hide method instead of calling it within the authenticator class (j). For now we will just arbitrarily choose to have the hide method called after 3 seconds. We will also change the hide method so that once the animation has finished we will set the message back to the default message.
send-screen.component.ts (3)
...
export class SendScreenComponent ... {
...
public failure = (message: string) => {
this._message = message;
setTimeout(() => {
this.hide();
}, 3000);
}
...
public hide = () => {
const animation = this._container.animate(...);
animation.onfinish = () => {
this._message = this._defaultMessage;
};
}
...
}
authenticator.component.ts (1)
...
export class AuthenticatorComponent ... {
...
private login = () => {
...
this._authenticatorHttpService.login(...,
() => {
},
() => {
...
this._sendScreen.hide(); // <- Remove this line
this._sendScreen.failure("Unable to login using the provided username and password.");
});
}
...
}
If we return to the browser and press the send button we should receive an error response from the server and the error message that we just defined should be displayed (k). You will also notice that although we are setting the message back to the default message after the hide animation finishes it will not be displayed until when/if the user interacts with the component. This is happening again because we are updating the message within the callback for the timeout. We could force angular to run an update but we don't need to here because an update will be run if the user interacts with the ui and of course they won't be able to see the message again until they do interact with it by pressing the send button.
Animating the message
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.pug
- send-screen.component.ts
-
authenticator
-
components
-
Source
With the changes that we have just made we are able to update the message being displayed to the user, at least if the server responds with an error, but the updating of the message is very abrupt. What we need to do is soften the change by using some animation. Of course since we will want to animate a DOM element we are back to needing to apply an id to the element in our template so that we can get a reference to it inside our typescript class (l). With the id being applied in the template we can turn to defining it as well as setting up the animation itself (m). Instead of creating a method to run the animation we will simply run it whenever the message is set using a setter property.
send-screen.component.pug (2)
div.send-screen(...)
div.message([attr.id]="messageId", ...)
send-screen.component.ts (4)
import { ..., ChangeDetectorRef, ... } from "@angular/core";
...
export class SendScreenComponent ... {
...
private _messageAnimationTiming: AnimationEffectTiming = {
duration: 250,
easing: "ease-in-out",
fill: "forwards"
}
...
private _messageDOM: HTMLElement = null;
...
public set message(message: string) {
const out = this._messageDOM.animate(
{
opacity: [1, 0]
}, this._messageAnimationTiming);
out.onfinish = () => {
this._message = message;
this._changeDetectorRef.detectChanges();
this._messageDOM.animate(
{
opacity: [0, 1]
}, this._messageAnimationTiming);
};
}
public get messageId() { return "assp-s-msg"; }
...
constructor(private readonly _changeDetectorRef: ChangeDetectorRef, ...) {
...
}
...
public ngAfterViewInit(): void {
...
this._messageDOM = this._domReader.findChildById(this._sendScreenDOM, this.messageId);
}
...
public failure = (message: string) => {
this.message = message;
...
}
...
}
Time to mess with an svg
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.pug
-
authenticator
-
components
-
Source
Since we have been setting everything up to respond to error response from the server it probably doesn't come as a surprise that the svg we are going to implement will be a variation on a . We will define our x using a path and line svg element (n).
send-screen.component.pug (3)
div.send-screen(...)
svg(version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="100%" viewBox="0 0 638 383" enable-background="new 0 0 638 383" xml:space="preserve" preserveAspectRatio="none")
| <path fill="none" stroke-width="20" d="M384.45,62.385
| C358.221,45.73,325.534,39.3,292.806,47.227c-63.666,15.42-103,79.895-87.58,143.561c15.42,63.667,79.896,103,143.562,87.581
| c63.666-15.42,102.999-79.896,87.579-143.562c-5.325-21.989-16.415-40.958-31.11-55.806l0,0L253.565,230.231"/>
| <line fill="none" stroke-width="20" x1="253.065" y1="95.268" x2="388.232" y2="230.436"/>
...
By adding a stroke to both of the svg elements in the browser developer tools we can see what our x really looks like (o).
How long are the path and line?
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.pug
- send-screen.component.ts
-
authenticator
-
components
-
Source
I know at this point I don't even have to say it but we are going to need to have access to the DOM elements of our svg so we need to apply some ids to them (p). Although unlike our recent DOM elements we are going to once again create some lazy getters for our x since we can hope that the majority of our users will never have to see, or at least only rarely see, an error response from the server (q). While we are at it we are also going to create properties that will hold the total length of both the path and the line and a method that we can use to calculate it.
send-screen.component.pug (4)
div.send-screen(...)
svg(...)
| <path [attr.id]="xmarkMainId" .../>
| <line [attr.id]="xmarkSupId" .../>
...
send-screen.component.ts (5)
...
export class SendScreenComponent ... {
...
private _pathLengthXmarkMain: number = null;
private _pathLengthXmarkSup: number = null;
...
private _xmarkMainDOM: SVGPathElement = null;
private _xmarkSupDOM: SVGLineElement = null;
private get pathLengthXmarkMain() {
if (this._pathLengthXmarkMain !== null) {
return this._pathLengthXmarkMain;
}
this._pathLengthXmarkMain = this.calculatePathLength(this.xmarkMainDOM);
return this._pathLengthXmarkMain;
}
private get pathLengthXmarkSup() {
if (this._pathLengthXmarkSup !== null) {
return this._pathLengthXmarkSup;
}
this._pathLengthXmarkSup = this.calculatePathLength(this.xmarkSupDOM);
return this._pathLengthXmarkSup;
}
private get xmarkMainDOM() {
if (this._xmarkMainDOM !== null) {
return this._xmarkMainDOM;
}
this._xmarkMainDOM = this._domReader.findChildById(this._sendScreenDOM, this.xmarkMainId) as any;
return this._xmarkMainDOM;
}
private get xmarkSupDOM() {
if (this._xmarkSupDOM !== null) {
return this._xmarkSupDOM;
}
this._xmarkSupDOM = this._domReader.findChildById(this._sendScreenDOM, this.xmarkSupId) as any;
return this._xmarkSupDOM;
}
...
public get xmarkMainId() { return "assp-s-x-mark-main"; }
public get xmarkSupId() { return "assp-s-x-mark-sup"; }
...
public ngAfterViewInit(): void {
...
console.log(this.pathLengthXmarkMain);
console.log(this.pathLengthXmarkSup);
}
...
private calculatePathLength = (svg: SVGElement) => {
return (svg as any).getTotalLength();
}
}
If you spend any time watching videos explaining how to animate and svg using the stroke dash offset, which is what we are going
to be doing, you will see the presenter using the getTotalLength
method just like we are
doing now. And when I was first working on this I added some console log statements just like we are doing now just to see if
it was working and I received messages along the lines shown in (r). Seeing this I
naturally believed everything was working until some time down the line I said to myself 'I wonder how this looks in firefox?'.
So I started up firefox and was surprised to see an error like (s). So once again
I was presented with the greatest joy of web programming, things being implemented differently in different browsers. Although this
may just be do to a change in the actual SVG spec itself it is still very annoying but we will solve this problem in the
next section.
Getting the total length
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.ts
-
authenticator
-
components
-
Source
Luckily for us I do have a degree in mathematics and I am able to put that vast amount of knowledge to work in
calculating the length of a line. Since the getTotalLength
method works,
at least for now, we will use it when it is available and specify the code to use if the element we are dealing
with is a line (t).
send-screen.component.ts (6)
import loDashIsNil = require("lodash/isNil");
...
export class SendScreenComponent ... {
...
private calculatePathLength = (svg: SVGElement) => {
if (loDashIsNil((svg as any).getTotalLength) === false) {
return (svg as any).getTotalLength();
}
switch (svg.tagName) {
case "line":
const x1 = (svg as SVGLineElement).x1.baseVal.value;
const x2 = (svg as SVGLineElement).x2.baseVal.value;
const y1 = (svg as SVGLineElement).y1.baseVal.value;
const y2 = (svg as SVGLineElement).y2.baseVal.value;
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
default:
throw new Error(`Unable to calculate the path length of type: ${svg.tagName}.`);
}
}
}
With those changes made we are now able to calculate the length of both elements even in firefox (u).
Don't forget to remove the console.log(this.pathLengthXmarkMain);
and
console.log(this.pathLengthXmarkSup);
statments.
Animating the stroke dash offset
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.ts
-
authenticator
-
components
-
Source
With all of the work we have just done we are now finally at the point where we can add the animation to our (v). For now once our animation is complete we will use a timeout to determine when the hide method should be called. We will eventually animate a timer element on the screen so the user know when the send screen will be hidden.
send-screen.component.ts (7)
...
export class SendScreenComponent ... {
...
public failure = (...) => {
this.message = message;
const mainDuration = 1500;
const animate = (element: SVGElement, length: number, duration: number) => {
element.style.strokeDasharray = `${length}`;
element.style.strokeDashoffset = `${length}`;
element.style.stroke = "red";
return element.animate(
{
strokeDashoffset: [length, 0]
},
{
duration: duration,
easing: "linear",
fill: "forwards"
}
);
}
const mainAnimation = animate(this.xmarkMainDOM, this.pathLengthXmarkMain, mainDuration);
mainAnimation.onfinish = () => {
const supAnimation = animate(this.xmarkSupDOM, this.pathLengthXmarkSup, mainDuration * this.pathLengthXmarkSup / this.pathLengthXmarkMain);
supAnimation.onfinish = () => {
setTimeout(() => {
this.hide(() => {
this.xmarkMainDOM.style.stroke = "none";
this.xmarkSupDOM.style.stroke = "none";
});
}, 3000);
};
};
}
public hide = (onfinish?: () => void) => {
...
animation.onfinish = () => {
...
if (loDashIsNil(onfinish) === false) {
onfinish();
}
};
}
...
}