Expiry page
Apply what you’ve learned from previous sections to the expiry field.
The problem
In this section, we will do for expiry what was done for cardNumber.
How to solve this problem
First:
- Write the
<input name="expiry"/>value to aBehaviorSubjectcalledthis.userExpiry$. - Create a
this.expiry$observable that is an array of two strings like["10","20"].
Second:
- Display an error message if the user has not entered the expiry correctly.
- Create a
expiryError$observable that represents this error. You can use this function to get the error fromuserExpiry$:(expiry) => { if (!expiry) { return 'There is no expiry. Format MM-YY'; } if ( expiry.length !== 2 || expiry[0].length !== 2 || expiry[1].length !== 2 ) { return 'Expiry must be formatted like MM-YY'; } }; expiryError$should be displayed within the<div class="message">element.
Finally:
- Only show the
expiryError$error if the user blurs the expiry input. Once the input blurs, we will update the displayed expiry error, if there is one, on every future keystroke. - Add class="is-error" to the input when it has an error.
- Create a
userExpiryBlurred$Subjectthat emits when theexpiryinput is blurred. - Create a
showExpiryError$that emits true when theexpiryerror should be shown.
What you need to know
You already know everything you need to know. Apply what you learned from
cardNumber$, cardError$ and showCardError$ to expiry$, expiryError$, and showExpiryError$.
The solution
Click to see the solution
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/3.19.0/core.js"></script>
<script src="https://unpkg.com/@angular/core@12.2.16/bundles/core.umd.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.11.4/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@12.2.16/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@12.2.16/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@12.2.16/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@12.2.16/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION } = ng.core;
const { BehaviorSubject, Subject, merge } = rxjs;
const { map, tap, scan } = rxjs.operators;
const cleanCardNumber = map((card) => {
if (card) {
return card.replace(/[\s-]/g, "");
}
});
const validateCard = map((card) => {
if (!card) {
return "There is no card";
}
if (card.length !== 16) {
return "There should be 16 characters in a card";
}
});
const log = (name) => {
return tap((value) => console.log(name, value));
};
function showOnlyWhenBlurredOnce(error$, blurred$) {
const errorEvents$ = error$.pipe(
map((error) => {
return { type: error ? "invalid" : "valid" };
})
);
const focusEvents$ = blurred$.pipe(
map((isBlurred) => {
return { type: isBlurred ? "blurred" : "focused" };
})
);
const events$ = merge(errorEvents$, focusEvents$);
const eventsToState = scan(
(previous, event) => {
switch (event.type) {
case "valid":
return { ...previous, isValid: true, showCardError: false };
case "invalid":
return {
...previous,
isValid: false,
showCardError: previous.hasBeenBlurred,
};
case "blurred":
return {
...previous,
hasBeenBlurred: true,
showCardError: !previous.isValid,
};
default:
return previous;
}
},
{
hasBeenBlurred: false,
showCardError: false,
isValid: false,
}
);
const state$ = events$.pipe(eventsToState);
return state$.pipe(map((state) => state.showCardError));
}
const expiryParts = map((expiry) => {
if (expiry) {
return expiry.split("-");
}
});
const validateExpiry = map((expiry) => {
if (!expiry) {
return "There is no expiry. Format MM-YY";
}
if (
expiry.length !== 2 ||
expiry[0].length !== 2 ||
expiry[1].length !== 2
) {
return "Expiry must be formatted like MM-YY";
}
});
@Component({
selector: 'my-app',
template: `
<form>
<div class="message" *ngIf="showCardError$ | async">{{ cardError$ | async }}</div>
<div class="message" *ngIf="showExpiryError$ | async">{{ expiryError$ | async }}</div>
<input
type="text"
name="cardNumber"
placeholder="Card Number"
(input)="userCardNumber$.next($event.target.value)"
(blur)="userCardNumberBlurred$.next(true)"
[class.is-error]="showCardError$ | async"
/>
<input
type="text"
name="expiry"
placeholder="MM-YY"
(input)="userExpiry$.next($event.target.value)"
(blur)="userExpiryBlurred$.next(true)"
[class.is-error]="showExpiryError$ | async"
/>
<input type="text" name="cvc" placeholder="CVC" />
<button>
PAY
</button>
</form>
UserCardNumber: {{ userCardNumber$ | async }} <br />
CardNumber: {{ cardNumber$ | async }} <br />
`
})
class AppComponent {
userCardNumber$ = new BehaviorSubject<string>();
userCardNumberBlurred$ = new Subject<boolean>();
userExpiry$ = new BehaviorSubject<[]>();
userExpiryBlurred$ = new Subject<boolean>();
constructor() {
this.cardNumber$ = this.userCardNumber$
.pipe(cleanCardNumber)
.pipe(log("cardNumber"));
this.cardError$ = this.cardNumber$.pipe(validateCard);
this.showCardError$ = showOnlyWhenBlurredOnce(this.cardError$, this.userCardNumberBlurred$);
this.expiry$ = this.userExpiry$.pipe(expiryParts);
this.expiryError$ = this.expiry$.pipe(validateExpiry);
this.showExpiryError$ = showOnlyWhenBlurredOnce(this.expiryError$, this.userExpiryBlurred$);
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [
BrowserModule,
CommonModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
<style>
@import url('https://fonts.googleapis.com/css?family=Raleway:400,500');
body {
background-color: rgba(8, 211, 67, 0.3);
padding: 2%;
font-family: 'Raleway', sans-serif;
font-size: 1em;
}
input {
display: block;
width: 100%;
box-sizing: border-box;
font-size: 1em;
font-family: 'Raleway', sans-serif;
font-weight: 500;
padding: 12px;
border: 1px solid #ccc;
outline-color: white;
transition: background-color 0.5s ease;
transition: outline-color 0.5s ease;
}
input[name='cardNumber'] {
border-bottom: 0;
}
input[name='expiry'],
input[name='cvc'] {
width: 50%;
}
input[name='expiry'] {
float: left;
border-right: 0;
}
input::placeholder {
color: #999;
font-weight: 400;
}
input:focus {
background-color: rgba(130, 245, 249, 0.1);
outline-color: #82f5f9;
}
input.is-error {
background-color: rgba(250, 55, 55, 0.1);
}
input.is-error:focus {
outline-color: #ffbdbd;
}
button {
font-size: 1em;
font-family: 'Raleway', sans-serif;
background-color: #08d343;
border: 0;
box-shadow: 0px 1px 3px 1px rgba(51, 51, 51, 0.16);
color: white;
font-weight: 500;
letter-spacing: 1px;
margin-top: 30px;
padding: 12px;
text-transform: uppercase;
width: 100%;
}
button:disabled {
opacity: 0.4;
background-color: #999999;
}
form {
background-color: white;
box-shadow: 0px 17px 22px 1px rgba(51, 51, 51, 0.16);
padding: 40px;
margin: 0 auto;
max-width: 500px;
}
.message {
margin-bottom: 20px;
color: #fa3737;
}
</style>