Friday, February 18, 2022

Printing with Angular

I'm in the middle of migrating an older project from Angular 7 to Angular 12. My team made a decision to start from scratch and build the application up using good patterns and practices this time (the original project had to be thrown together rather quickly to meet a business deadline; so it goes). This week I arrived at a piece of code intended to print a receipt. The receipt itself is an isolated component that is typically (but not always) displayed in a modal (we use ng-bootstrap). In the old version we have a rather sloppy (but effective) piece of code that opens a new window and writes the HTML contents of the receipt into the new window, then immediately invokes the print function. It does work, but man is it unseemly. At first I tried to just migrate that code over, but the newer version of TypeScript didn't like some things about it (and I didn't spend much time trying to figure out what it didn't like). For posterity, here's the old code.


print() {
  let printContents, popupWin;
  printContents = document.getElementById('print-section').innerHTML
  printContents = `<div div class="text-center mb-4 mt-4"><img src="https://logo.url" class="receipt-logo"></div>${printContents}`;
  popupWin = window.open('/', '_blank', 'top=0,left=0,height=100%,width=auto');
  popupWin.document.open();
  popupWin.document.write(`
    <html>
      <head>
        <title>Receipt</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
        <style>
          @page {
            size: auto;   /* auto is the initial value */
            margin: 5%;
          }
          body {
            padding-top: 50px;
          }
          .btn {
            display: none;
          }
          .text-primary {
            color: #333333;
          }
          .receipt-logo {
            width: 200px;
          }
        </style>
      </head>
      <body onload="window.print();setTimeout(function() { window.close(); }, 500); // Timout is Safari hack. Safari dosn't wait for print to close window">${printContents}</body>
    </html>
  );
  popupWin.document.close();
}

I want to stress once more that this absolutely does work, but... well, just look at it. It's unpleasant. Since we're taking the time to rewrite the entire application I wanted a better way to do this, but Angular sure didn't make it easy.

The TL;DR of what I did is pretty simple: I created a component that injects another component into a directive that sits as a sibling to my root component.

I'll first acknowledge that what I'm doing here is so common that the Angular docs actually have a tutorial on how to do it, which you can find here. I had to make some modifications to that code to make it work for me, which I based on a previous blog post I created, which you can find here.

Now for the details. There are a few factors I had to consider.
  1. The component's contents must be printable while the component is hosted in a modal and while it is hosted outside a modal
  2. I don't want to reload the entire contents of the receipt just so I can print it, but I'm OK with using data that's in state (which is how the application is constructed) to generate new HTML
  3. I want to be able to control what is printed on the receipt, and potentially have that be different than what is displayed in the UI
My app.component is pretty basic, but here's what it looked like before I added in my print stuff:

template: `<router-outlet></router-outlet><app-footer></app-footer>`

Side note: within the <router-outlet> the very first component will always be <app-wrapper>. I did that because I have a public section and a secure (behind a login) section of my app. Each of those sections has a different wrapper, but both use the <app-wrapper> selector.

The first thing I need is a directive that will host my receipt component.

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appPrint]'
})
export class PrintDirective {
  constructor(public viewContainerRef: ViewContainerRef) { }
}

Next, I need a service so I know which component will be printed (this could be eliminated if you only always want to print the same component, but I already know that I'll have another component down the road that will need to be printed as well).

import { Injectable, Type } from '@angular/core';

import { BehaviorSubject } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';

import { PrintableComponent } from '../../../data/models';

@Injectable({
  providedIn: 'root'
})
export class PrintStateService {
  public printNotifier: BehaviorSubject<string | null> = new BehaviorSubject<string | null>(null);
  public type?: Type<PrintableComponent>;

  public clear(): void {
    this.type = undefined;
    this.printNotifier.next(null);
  }

  public set(type: Type<PrintableComponent>): void {
    this.type = type;
    this.printNotifier.next(uuidv4());
  }
}

Now that I have the directive and the service I need the component to actually instantiate the component and plug it into the directive.

import { AfterViewInit, Component, ComponentFactoryResolver, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core'

import { Subject } from 'rxjs'
import { takeUntil } from 'rxjs/operators';

import { PrintStateService } from '../../../core';
import { PrintDirective } from '../../directives/print.directive';


  
@Component({
  selector: 'app-print',
  template: '<ng-template atlasPrint><ng-template>',
  styleUrls: ['./print-component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class PrintComponent implements AfterViewInit, OnDestroy {
  private takeSubscriptionsUntil: Subject<void> = new Subject();

  @ViewChild(PrintDirective, {static: true}) printHost!: PrintDirective;

  constructor (
    private componentFactoryResovler: ComponentFactoryResolver,
    private printStateService: PrintStateService
  ) { }

  public ngAfterViewInit(): void {
    this.printStateService.printNotifier.pipe(takeUntil(this.takeSubscriptionsUntil))
      .subscribe((notificationId: string | null) => {
        if (!!notificationId) {
          this.loadComponent();
        } else {
          this.printHost.viewContainerRef.clear();
        }
      })
  }

  public ngOnDestroy(): void {
    this.takeSubscriptionsUntil.next();
    this.takeSubscriptionsUntil.complete();
  }

  private loadComponent(): void {
    if (!this.printStateService.type( {
      return;
    }

    const viewContainerRef = this.printHost.viewContainerRef;
    viewContainerRef.clear();

    const factory = this.componentFactoryResolver.resolveComponentFactory(this.printStateService.type);
    const componentRef = viewContainerRef.createComponent(factory);
    componentRef.instance.isPrinted = true;

    // this is wrapped in a setTimeout because not doing that causes the data to not be loaded into the injected component
    // so basically wrapping it like this gives the lifecycle the necessary order to populate the injected component so there's
    // actually something to display in the print preview window
    setTimeout(() => window.print(), 1);
  }
}

A little bit of styling on the print component allows us to hide everything except the print component when printing is invoked.

@media screen {
  app-print {
    display: none !important;
  }
}

@media print {
  .modal, .modal-backdrop, .app-wrapper, .app-footer, button {
    display: none !important;
  }
}

Once I have all of that in place I can modify the app.component to use the print component I created in the previous step.

template: `<router-outlet></router-outlet><app-print></app-print><app-footer></app-footer>`

The only thing left now is to tell the print button on the receipt component to load the receipt component anew into the print component and trigger the browser's print functionality.

import { Component, Input, OnDestroy, OnInit } from '@angular/core'

import { Subject } from 'rxjs'
import { takeUntil } from 'rxjs/operators';

import { OrderStatuses, OrderTypes, PaymentTypes } from '../../../data/enums';
import { LoadingStateService, PrintStateService, ReceiptStateService } from '../../../core';
import { PrintableComponent, Receipt } from '../../data/models';


  
@Component({
  selector: 'app-receipt',
  templateUrl: './receipt.component.html',
  styleUrls: ['./receipt-component.scss']
})
export class ReceiptComponent implements OnInit, OnDestroy, PrintableComponent {
  @Input() isModal: boolean = false;
  @Input() isPrinted: boolean = false;

  private takeSubscriptionsUntil: Subject<void> = new Subject();

  public receipt: Receipt | null | undefined;
  public isLoading = false;
  public OrderStatuses: typeof OrderStatuses = OrderStatuses;
  public OrderTypes: typeof OrderTypes = OrderTypes;
  public PaymentTypes: typeof PaymentTypes = PaymentTypes;

  constructor (
    private printStateService: PrintStateService,
    private receiptStateService: ReceiptStateService
  ) { }

  public ngOnInit(): void {
    this.receiptStateService.stateChanged.pipe(takeUntil(this.takeSubscriptionsUntil))
      .subscribe((state: StoreState) => state?.receipt);

  }

  public ngOnDestroy(): void {
    this.takeSubscriptionsUntil.next();
    this.takeSubscriptionsUntil.complete();
  }

  public print(): void {
    this.printStateService.set(ReceiptComponent);
  }
}

Oops, I forgot to show what the PrintableComponent interface looks like. Here it is.

export interface PrintableComponent {
  isPrinted: boolean;
}

Et voila! Clicking the print button on the receipt pushes a new instance of the receipt component into the print directive on the print component, then triggers the browser's print functionality through window.print. Pretty neat!