#27 Finishing the Send Screen
Thursday, April 5, 2018
In this video we will add and animate a timer svg element to our send screen that will indicate to the user when the screen will be hidden or an action will be taken. We will also add a new svg element that will be used when the server responds with a success status code. We will end by adding a background element whose background color we will animate to indicate whether the current request was successful or not.
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.
We need to add a timer
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.pug
- send-screen.component.ts
-
authenticator
-
components
-
Source
It's very important that we try to eliminate any possibility of the user wondering if our application or components are actually doing anything. To that end we need to add a timer element that will let the user know when our send screen will perform an action after notifying them whether a request was successful or not. The timer that we will use is another svg line that we will add at the top of the svg element (a). Of course as usual in order to animate it we will add the familiar fields and properties to our typescript class (b). It also makes sense of course to create a helper method that we can call from different methods in order to animate the timer.
send-screen.component.pug (1)
div.send-screen(...)
svg(...)
| <line [attr.id]="timerId" fill="none" stroke-width="4" x1="0" y1="2" x2="638" y2="2"/>
...
send-screen.component.ts (1)
...
export class SendScreenComponent ... {
...
private _pathLengthTimer: number = null;
...
private _timerDOM: SVGLineElement = null;
...
public get timerId() { return "assp-s-timer"; }
...
public ngAfterViewInit(): void {
...
this._timerDOM = this._domReader.findChildById(this._sendScreenDOM, this.timerId) as any;
this._pathLengthTimer = this.calculatePathLength(this._timerDOM);
}
...
public failure = (message: string) => {
...
mainAnimation.onfinish = () => {
...
supAnimation.onfinish = () => {
this.animateTimer(() => {
this.xmarkMainDOM.style.stroke = "none";
this.xmarkSupDOM.style.stroke = "none";
});
};
};
}
...
private animateTimer = (onhide?: () => void) => {
this._timerDOM.style.stroke = "white";
this._timerDOM.style.strokeDasharray = `${this._pathLengthTimer}`;
const animation = this._timerDOM.animate(
{
strokeDashoffset: [0, this._pathLengthTimer]
},
{
duration: 3000,
easing: "linear",
fill: "forwards"
});
animation.onfinish = () => {
this._timerDOM.style.stroke = "none";
this.hide(onhide);
};
}
}
In (c) I have adjust the stoke color to be blue so that we can see the placement of the timer and in (d) we can see what it looks like after having run for about a second or so.
At some point we are going to succeed
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.pug
- send-screen.component.ts
-
authenticator
-
components
-
Source
So far the only thing our send screen is capable of reacting to is an error response from the server. Now it's time to think
positively and create elements and methods that we can use if the server responds with a success code. The first step that
we will take is to create a checkmark element within our svg (e). After taking the
usual steps to be able to animate our checkmark we need to also create a success
method that can be called with a message and to create an animatePath
method that both our
(f).
send-screen.component.pug (2)
div.send-screen(...)
svg(...)
...
| <path [attr.id]="checkmarkId" stroke-width="20" fill="none" d="M406.298,80.19c-21.572-22.361-51.826-36.305-85.501-36.305
| c-65.506,0-118.912,53.405-118.912,118.912s53.405,118.912,118.912,118.912c65.507,0,118.912-53.405,118.912-118.912
| c0-22.625-6.313-43.671-17.101-61.561l0,0L311.326,212.782l-54.983-54.984"/>
send-screen.component.ts (2)
...
export class SendScreenComponent ... {
...
private _checkmarkDOM: SVGPathElement = null;
...
private _pathLengthCheckmark: number = null;
...
public get checkmarkId() { return "assp-c-mark"; }
...
public ngAfterViewInit(): void {
...
this._checkmarkDOM = this._domReader.findChildById(this._sendScreenDOM, this.checkmarkId) as any;
...
this._pathLengthCheckmark = this.calculatePathLength(this._checkmarkDOM);
...
}
...
public failure = (message: string) => {
...
const stroke = "red";
const mainAnimation = this.animatePath(this.xmarkMainDOM, this.pathLengthXmarkMain, mainDuration, stroke);
mainAnimation.onfinish = () => {
const supAnimation = this.animatePath(this.xmarkSupDOM, this.pathLengthXmarkSup, mainDuration * this.pathLengthXmarkSup / this.pathLengthXmarkMain, stroke);
...
};
}
...
public success = (message: string) => {
this.message = message;
const mainDuration = 1500;
const stroke = "green";
const animation = this.animatePath(this._checkmarkDOM, this._pathLengthCheckmark, mainDuration, stroke);
animation.onfinish = () => {
this.animateTimer(() => {
this._checkmarkDOM.style.stroke = "none";
});
};
}
...
private animatePath = (element: SVGElement, length: number, duration: number, stroke: string) => {
element.style.strokeDasharray = `${length}`;
element.style.strokeDashoffset = `${length}`;
element.style.stroke = stroke;
return element.animate(
{
strokeDashoffset: [length, 0]
},
{
duration: duration,
easing: "linear",
fill: "forwards"
});
}
...
}
By adding a temporary green stroke to our checkmark we can see what it looks like (g).
Our login method is never happy
-
WebUi
-
Pages
-
Account
- Login.cshtml.cs
-
Account
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Pages
As it stands now the way we have implemented our login action we will always receive an error response so it is time
to modify it so that we can test how our send screen will respond to a success code (h).
After that is done we need to return to our authenticator component and add the call to the
success
method (i).
Login.cshtml.cs (1)
...
namespace WebUi.Pages.Account
{
public class LoginModel : PageModel
{
...
public IActionResult OnPost(...)
{
...
if (request.Username == "chris.jamell")
{
ModelState.AddModelError("Username", "Pick a better username.");
ModelState.AddModelError("Username", "A random error.");
ModelState.AddModelError("Password", "Another random error.");
}
...
}
}
}
authenticator.component.ts (1)
...
export class AuthenticatorComponent ... {
...
private login = () => {
...
this._authenticatorHttpService.login(...,
() => {
this._sendScreen.success("Login was successful and you will be redirected shortly.");
},
() => {
...
});
}
...
}
With those changes made if we send in valid information to the server we should receive a success response which should cause our send screen to look like (j).
The name of the game is change
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.pug
- send-screen.component.scss
- send-screen.component.ts
-
authenticator
-
components
-
Source
As you have seen, if you have checked out the finished example yourself or seen me show it in a video, the way that I styled it was to apply either a red or green color to the different paths indicating whether the server responded with a success or error code. I also added an additional element to the background of the svg that also adjusted its color accordingly. As I believe I have mentioned in the past I can't help changing things around so what we are about to do should not come as a complete surprise. Instead of changing the color of the paths we are going to add a background element that will animate in with the appropriate color and the other elements of the svg will just use white for their strokes. To that end we need to add a background element to our template (k). Next we need to style it so that it covers the entire send screen and is positioned behind all the other elements (l). Now that the background element exists and is styled we need to add the code that will animate it correctly (m).
send-screen.component.pug (3)
div.send-screen(...)
div.send-screen-background([attr.id]="backgroundId")
...
send-screen.component.scss (1)
...
.send-screen {
...
.send-screen-background {
@include position(absolute, 0 0 0 0);
z-index: -1;
}
...
}
send-screen.component.ts (3)
...
export class SendScreenComponent ... {
...
private _backgroundDOM: HTMLElement = null;
...
public get backgroundId() { return "assp-s-bckgrnd"; }
...
public ngAfterViewInit(): void {
...
this._backgroundDOM = this._domReader.findChildById(this._sendScreenDOM, this.backgroundId);
...
}
...
public failure = (message: string) => {
...
this.animateBackground("red");
...
mainAnimation.onfinish = () => {
...
supAnimation.onfinish = () => {
this.animateTimer(() => {
...
this._backgroundDOM.style.backgroundColor = "transparent";
});
};
};
}
...
public success = (message: string) => {
...
this.animateBackground("green");
...
animation.onfinish = () => {
this.animateTimer(() => {
...
this._backgroundDOM.style.backgroundColor = "transparent";
});
};
}
...
private animateBackground = (color: string) => {
this._backgroundDOM.style.opacity = "0";
this._backgroundDOM.style.backgroundColor = color;
this._backgroundDOM.animate(
{
opacity: [0, 1]
},
{
duration: 1000,
fill: "forwards"
});
}
...
}
Not surprisingly if we add a temporary green background color to our background element we should see (n).
How about using the same colors we have already been using?
-
WebUi
-
Source
-
components
-
authenticator
- _variables.scss
- authenticator.component.scss
-
authenticator
-
components
-
Source
If at some point while we have been adding colors to our elements in our typescript class you said to yourself 'I don't like this we have already defined the colors that we are using elsewhere' I completely agree with you so it's time to fix this situation. The first step in our solution is to define a variables scss file that we can import into both our send screen and authenticator style files (o). With this file created and our colors added to it we can remove the color definitions from our authenticator scss file and import the variables file (p).
_variables.scss
$awaiting-confirmation-color: #E87E04 !default;
$indeterminate-color: #BDC3C7 !default;
$invalid-color: #D91E18 !default;
$valid-color: #26A65B !default;
authenticator.component.scss (1)
...
@import "_variables.scss";
...
$awaiting-confirmation-color: #E87E04 !default; // <- Remove
$indeterminate-color: #BDC3C7 !default; // <- Remove
$invalid-color: #D91E18 !default; // <- Remove
$valid-color: #26A65B !default; // <- Remove
...
Coloring our svg elements using css
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.pug
- authenticator.component.scss
- authenticator.component.ts
-
authenticator
-
components
-
Source
Now it's time to adjust how we are applying the color to our svg elements. The first step is to add a
css class to the xmark and checkmark elements (q). In order
to add the correct color to the elements we will be adding an additional css class to them depending on
whether the server responds with a success or error code (r).
The last thing we need to do to make this work is to modify our
animateBackground
method so that it applies a class to our
background instead of changing its background color directly (s).
send-screen.component.pug (4)
div.send-screen(...)
div.send-screen-background(...)
svg(...)
...
| <path [attr.id]="xmarkMainId" ... class="sf-indicator" ... />
| <line [attr.id]="xmarkSupId" ... class="sf-indicator" ... />
| <path [attr.id]="checkmarkId" ... class="sf-indicator" ... />
...
send-screen.component.scss (2)
...
@import "_variables.scss";
.send-screen {
...
.send-screen-background {
...
&.valid {
background-color: $valid-color;
}
&.invalid {
background-color: $invalid-color;
}
}
svg {
.sf-indicator {
stroke-width: 10;
}
}
...
}
send-screen.component.ts (4)
...
export class SendScreenComponent ... {
...
public failure = (message: string) => {
const backgroundClass = "invalid";
...
const stroke = "white"; //<- Remove
this.animateBackground(backgroundClass);
...
const mainAnimation = this.animatePath(this.xmarkMainDOM, this.pathLengthXmarkMain, mainDuration);
mainAnimation.onfinish = () => {
const supAnimation = this.animatePath(this.xmarkSupDOM, this.pathLengthXmarkSup, mainDuration * this.pathLengthXmarkSup / this.pathLengthXmarkMain);
supAnimation.onfinish = () => {
this.animateTimer(() => {
...
this._backgroundDOM.classList.remove(backgroundClass);
});
};
};
}
...
public success = (message: string) => {
const backgroundClass = "valid";
...
const stroke = "white"; //<- Remove
this.animateBackground(backgroundClass);
const animation = this.animatePath(this._checkmarkDOM, this._pathLengthCheckmark, mainDuration);
animation.onfinish = () => {
this.animateTimer(() => {
...
this._backgroundDOM.classList.remove(backgroundClass);
});
};
}
...
private animateBackground = (cssClass: string) => {
...
this._backgroundDOM.classList.add(cssClass);
...
}
private animatePath = (element: SVGElement, length: number, duration: number) => {
...
element.style.stroke = "white";
return element.animate(...);
}
...
}
Helps if you can read the message
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
-
authenticator
-
components
-
Source
It's time to work on the message for our send screen to make it easier to read and maybe a bit more attractive. The first thing we need to do is modify the login failure message. We are doing this because I want it to occupy only one line at the default font size (t).
authenticator.component.ts (2)
...
export class AuthenticatorComponent ... {
...
private login = () => {
...
this._authenticatorHttpService.login(this._xhrData,
...,
() => {
...
this._sendScreen.failure("Error using the provided username and password.");
});
}
...
}
-
WebUi
-
Source
-
components
-
authenticator
- send-screen.component.pug
- send-screen.component.scss
- send-screen.component.ts
-
authenticator
-
components
-
Source
Next we need to modify the template in regards to the message so that we can add a background color and not have it effected by the fading in and out animations of the message text (u). Now we will modify the styling for the message and its text (v). As you can see we will change the color of the message text depending on an additional valid or invalid class. In order to apply this class we will change how we are setting the message text by removing the setter and adding a method (w).
send-screen.component.pug (5)
div.send-screen(...)
...
div.message
div.message-text([attr.id]="messageId", [innerHtml]="message")
send-screen.component.scss (3)
...
.send-screen {
...
.message {
...
@include padding(em(8px) null);
...
background-color: $dark-gray;
font-weight: 600;
color: $light-gray;
.message-text {
&.invalid {
color: $invalid-color;
}
&.valid {
color: $valid-color;
}
}
}
}
send-screen.component.ts (5)
...
export class SendScreenComponent ... {
...
public failure = (message: string) => {
...
this.setMessage(message, backgroundClass);
...
mainAnimation.onfinish = () => {
...
supAnimation.onfinish = () => {
this.animateTimer(() => {
...
this._messageDOM.classList.remove(backgroundClass);
});
};
};
}
...
public success = (message: string) => {
...
this.setMessage(message, backgroundClass);
...
animation.onfinish = () => {
this.animateTimer(() => {
...
this._messageDOM.classList.remove(backgroundClass);
});
};
}
...
private setMessage = (message: string, cssClass: string) => {
const out = this._messageDOM.animate(
{
opacity: [1, 0]
},
this._messageTiming);
out.onfinish = () => {
this._messageDOM.classList.add(cssClass);
this._message = message;
this._changeDetectorRef.detectChanges();
this._messageDOM.animate(
{
opacity: [0, 1]
},
this._messageTiming);
};
}
}
With those changes made our send screen now looks like (x) by default while we are waiting for a response from the server and like (y) when there was an error and like (z) when the request was successful.