CVC page

Apply what you've learned from previous sections to the cvc field.

The problem

In this section, we will do for cvc what was done for cardNumber and expiry.

How to solve this problem

First:

  • Write the <input name="cvc"/> value to a BehaviorSubject called this.userCVC.
  • Create a this.cvc observable that is just a reference to this.userCVC. We are not going to worry about input cleanup for this field.

Second:

  • Display an error message if the user has not entered the cvc correctly.
  • Create a cvcError observable that represents this error. You can use this function to get the error from userCVC:
    (cvc) => {
        if (!cvc) {
            return "There is no CVC code";
        }
        if (cvc.length !== 3) {
            return "The CVC must be at least 3 numbers";
        }
        if (isNaN(parseInt(cvc))) {
            return "The CVC must be numbers";
        }
    }
    
  • cvcError should be displayed within the <div class="message"> element.

Finally:

  • Only show the cvcError error if the user blurs the cvc input. Once the input blurs, we will update the displayed cvc error, if there is one, on every future keystroke.
  • Add class="is-error" to the input when it has an error.
  • Create a userCVCBlurred Subject that emits when the cvc input is blurred.
  • Create a showCVCError that emits true when the cvc error 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 cvc, cvcError, and showCVCError.

The solution

<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@6.0.5/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@6.0.5/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@6.0.5/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@6.0.5/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@6.0.5/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 = function(name) {
    return tap(value => console.log(name, value))
}

function showOnlyWhenBlurredOnce(errorObservable, blurredObservable) {

    const errorEvents = errorObservable.pipe(
        map((error) => {
            return {type: error ? "invalid" : "valid"}
        })
    );

    const focusEvents = blurredObservable.pipe(
        map((isBlurred) => {
            return {type: isBlurred ? "blurred" : "focused"};
        })
    );

    var 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
    });

    var 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";
    }
});

const validateCVC = map((cvc) => {
    if (!cvc) {
        return "There is no CVC code";
    }
    if (cvc.length !== 3) {
        return "The CVC must be at least 3 numbers";
    }
    if (isNaN(parseInt(cvc))) {
        return "The CVC must be numbers";
    }
});

@Component({
    selector: 'my-app',
    template: `
        <form>

            <div class="message"
                *ngIf="(showCardError | async)">{{ (cardError | async) }}</div>

            <div class="message"
                *ngIf="(showExpiryError | async)">{{ (expiryError | async) }}</div>

            <div class="message"
                *ngIf="(showCVCError | async)">{{ (cvcError | 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"
                (input)="userCVC.next( $event.target.value )"
                (blur)="userCVCBlurred.next( true )"
                [class.is-error]="showCVCError | async"/>

            <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<Array>();
    userExpiryBlurred = new Subject<Boolean>();

    userCVC = new BehaviorSubject<String>();
    userCVCBlurred = 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);

        this.cvc = this.userCVC;
        this.cvcError = this.cvc.pipe(validateCVC);
        this.showCVCError = showOnlyWhenBlurredOnce(this.cvcError, this.userCVCBlurred);
    }
}

// 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>