Advertisement

#26 Animating the Stroke Dashoffset

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.

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
      • sass
        • 2-base
          • _functions.scss

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;
}
(a) Moving our em function to a separate file that we will be able to reference throughout our project.

authenticator.component.scss (1)

...
@import "../../sass/2-base/_functions.scss";
...
.authenticator {
    ...
    /*overflow: hidden;*/
    ...
}
(b) If we comment out the overflow hidden statement in our authenticator's style file it will make our life a lot easier when making changes to the send screen component.

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(...);
    }
}
(c) I made a mistake in the last article in calling the DOM element that we were referencing.

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).

Once the overflow hidden statement is removed the send screen should
            once again be visible in its hidden position.
(d) Once the overflow hidden statement is removed the send screen should once again be visible in its hidden position.

Conveying a message to the user

  • WebUi
    • Source
      • components
        • authenticator
          • send-screen.component.pug
          • send-screen.component.scss
          • send-screen.component.ts

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")
(e) Creating a div that we can bind its inner html property to a property on our typescript class which will allow us to present a message to the user.

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);
    }
}
(f) The styling that we will be using for our message.

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;
    }
    ...
}
(g) We need to add a property to our typescript class that can be displayed by our template.

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.

Image showing that our default message is being styled and displayed as we expect.
(h) Image showing that our default message is being styled and displayed as we expect.

Responding to a failure

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts
          • send-screen.component.ts

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;
        };
    }
    ...
}
(i) Introducing the failure method which as name implies we will call if the server responds with an error code.

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.");
            });
    }
    ...
}
(j) We just need to remove our call to the hide method of the send screen and replace it with a call to the failure method and provide an appropriate message.

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.

Image showing that we are able to update the message being displayed by
            calling the failure method.
(k) Image showing that we are able to update the message being displayed by calling the failure method.

Animating the message

  • WebUi
    • Source
      • components
        • authenticator
          • send-screen.component.pug
          • send-screen.component.ts

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", ...)
(l) Just need to apply an id to our message element so that we can animate it.

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;
        ...
    }
    ...
}
(m) Here we are going to run our message animation whenever we set the method through the setter property. Don't for get to update the failure method to use the setter and not the field directly.
Advertisement

Time to mess with an svg

  • WebUi
    • Source
      • components
        • authenticator
          • send-screen.component.pug

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"/>
    ...
(n) The svg code that defines what our error symbol will look like.

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).

If we add a stroke property to both the path and line in the browser developer tools we can
            see what the svg looks like.
(o) If we add a stroke property to both the path and line in the browser developer tools we can see what the svg looks like.

How long are the path and line?

  • WebUi
    • Source
      • components
        • authenticator
          • send-screen.component.pug
          • send-screen.component.ts

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" .../>
    ...
(p) Adding an id to both the path and line elements.

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();
    }
}
(q) Adding the ids, lazy getters and a method for calculating the length of the path and line.

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.

We are able to get the lengths of both the path and the line in
            chrome using the getTotalLength method.
(r) We are able to get the lengths of both the path and the line in chrome using the getTotalLength method.
The getTotalLength method
        only works for the path in firefox and not the line.
(s) The getTotalLength method only works for the path in firefox and not the line.

Getting the total length

  • WebUi
    • Source
      • components
        • authenticator
          • send-screen.component.ts

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}.`);
        }
    }
}
(t) Calculating the length of an svg element using the getTotalLength method when it's available and doing it ourselves if the element is a line.

With those changes made we are now able to calculate the length of both elements even in firefox (u).

Image showing the length of both the path and line elements in firefox.
(u) Image showing the length of both the path and line elements in firefox.

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

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();
            }
        };
    }
    ...
}
(v) We are finally in a position to animate our .
Exciton Interactive LLC
Advertisement