Clean card number page

Learn how to map an observable value to another observable value.

Video

Who has time to read? This video covers the content on this page. Watch fullscreen.

The problem

In this section, we will:

  • Write out the user entered card number without spaces or dashes.

How to solve this problem

  • Create a this.cardNumber$ observable that contains the text of the userCardNumber input without spaces (\s) or dashes (-).
  • Create a cleanCardNumber operator that maps the this.userCardNumber$ to this.cardNumber$.
  • Write out the value of the this.cardNumber$ observable like:
    CardNumber: {{ cardNumber$ | async }} <br />
    

What you need to know

  • RxJS has operators (an older but better explanation) that are used to convert one observable to another observable. In RxJS, you typically create your own operators by using operator generator functions like map.

    Those operators are passed to source.pipe(operator) to convert the source observable to a new observable.

    The following uses map to create a mapToNumber operator:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.4.0/rxjs.umd.min.js"></script>
    <script type="typescript">
      const { Subject } = rxjs;
      const { map } = rxjs.operators;
    
      const mapToNumber = map(value => +value);
    
      const source = new Subject();
      const number = source.pipe(mapToNumber);
    
      number.subscribe(console.log);
    
      source.next(true); // logs 1
      source.next("2");  // logs 2
    </script>
    
  • Use replace(/[\s-]/g, "") method to clean the card number.
  • Write out the cardNumber$ after the userCardNumber like:
    CardNumber: {{ cardNumber$ | async }} <br />
    

The debugging section will show how to verify the results in details.

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 } = rxjs;
  const { map } = rxjs.operators;

  const cleanCardNumber = map((card) => {
    if (card) {
      return card.replace(/[\s-]/g, "");
    }
  });

  @Component({
    selector: 'my-app',
    template: `
      <form>
        <div class="message"></div>

        <input
          type="text"
          name="cardNumber"
          placeholder="Card Number"
          (input)="userCardNumber$.next($event.target.value)"
        />

        <input type="text" name="expiry" placeholder="MM-YY" />

        <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>();

    constructor() {
      this.cardNumber$ = this.userCardNumber$.pipe(cleanCardNumber);
    }
  }

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