Background (Image)

Transitioning background cover images in Angular applications is no different to the regular approach - create a temporary img element in JavaScript, add a load event listener, assign the image url to img.src - starting the async loading of the image, and when the image is loaded and cached by the browser (triggering the load event handler) assign the image url and transition class to your HTMLElement. Simple, except for the edge cases around Angular route reuse!

Setup

When using route params e.g. path: 'reuse/:id', the associated route component is reused - when this occurs and the updated @Input's are applied, we have to make sure that they are processed correctly. For the scenario being described henceforth, said @Input shall be an imageURL, the value of which shall be applied to a <div> background image (I don't speak like that in real life btw):

Parent Component

HomePageComponent

@Component({
  selector: 'bc-home-page',
  template: `
    <div class="page-container">
      <bc-page-header [imageURL]="imageURL" [forceReflow]="forceReflow">
        <h1 class="heading-text">{{title}}</h1>
      </bc-page-header>
      <div>Some Content below the fold</div>
    </div>
  `,
  styleUrls: ['./home-page.component.scss']
})

The routing params are managed by the HomePageComponent, subscribed to in the constructor:

constructor

this.route.data.subscribe((data: { resolvedData: IResolvedData }) => {
  this.imageURL = data.resolvedData.imageURL;
  this.title = data.resolvedData.title;
});

Header Child Component

The header component template, and relevant ngOnChanges method

PageHeaderComponent bc-page-header

<header class="page-header" [style.background-color]="backgroundColor">
   <!-- absolute left/top 0px, w/h 100% -->
   <div class="cover"
        [ngStyle]="{
          'background-image': (revealImage && imageURL) ? 'url(' + imageURL  + ')' : ''
        }"
        [ngClass]="{
           'reveal-image' : revealImage
        }">
   </div>
   <!-- project the title. Inherits text-align center by default-->
   <ng-content></ng-content>
</header>
ngOnChanges(changes: SimpleChanges): void {
  // ngOnInit not called for reused routing components, so
  // need to use ngOnChanges with a check for the particular property
  if (changes['imageURL']) {
    if (this.isBrowser) {
      this.updateImage();
    }
  }
}

The updateImage() function would generally complete the following steps - which work perfectly if not reusing routes:

  1. Hide the image - "background-image: url(''); opacity: 0; visibility: hidden"
  2. Asynchronously load the image
  3. Apply the url, set visibility to visible, apply a transition for opacity: 1

Browser Button Problem

Clicking the Back & Forward buttons in Chrome (v67 at time of writing) caused the transitions to fail. The image displayed with a transition on first load, and on the first reuse of the route - but subsequent routing with the Back & Forward buttons caused the images to appear immediately with no transition effect

Note: the problem only occurs when browser buttons are used

Transition_Not_Triggering transition issue

After digging into the problem, I found it was caused by a combination of the following:

  • Images were already cached by the browser, so no delay in loading them
  • The CSS class to hide the image after component reuse was being added to the element, but the corresponding styles were not processed before the reveal-image class was added
  • This meant that the only CSS styles being applied were the reveal-image class styles, so there were no initial "hide-image" styles to transition from

Solution

I got around the Back/Forward button issue by making sure the reveal-image CSS class was removed and processed before the image loading began. This required a combination of ChangeDetectionRef.detectChanges and a forced reflow to recalculate the CSS styles that were scheduled to be applied

Transitions_Applied 
transition with reflow

Updating the Styles

PageHeaderComponent excerpt

@Input() backgroundColor = 'black';
@Input() imageURL: string;
@Input() forceReflow: boolean;

ngOnChanges(changes: SimpleChanges): void {
  // ngOnInit not called for reused routing components, so
  // need to use ngOnChanges with a check for the particular property
  if (changes['imageURL']) {
    if (this.isBrowser) {
      this.updateImage();
    }
  }
}

updateImage() {
  this.revealImage = false;
  if (this.forceReflow) {
    // Apply data binding - removes reveal-image class
    this.changeDetectorRef.detectChanges();
    // Force a Recalculate Style so the 'reveal-image' style is applied
    // The div.cover element will then have { opactiy: 0 and visibility: hidden)
    this.forceRecalculateStyle();
  }

  // Load the image into the browser cache first. When loaded,
  // apply the transition css class and background-image:url() at the
  // same time by setting revealImage=true
  if (this.imageURL) {
    const image: HTMLImageElement = document.createElement('img');
    let self = this;
    image.addEventListener('load', function handleImageLoad() {
      self.revealImage = true;
      image.removeEventListener('load', handleImageLoad);
    });
    image.src = this.imageURL; // begin loading image (to browser cache)
  }
}

private forceRecalculateStyle() {
  return window.scrollY;
}

UglifyJS Angular CLI uses UglifyJS, which carries out pretty aggressive optimization and unreferenced code removal. The window.scrollY, if not called via a function or assignment, was being removed by UglifyJS in --prod mode

StackBlitz

Open on StackBlitz and test with Forced Reflow checked/unchedked - use Open in New Window (Live) option form the StackBlitz menu (otherwise the page will refresh when Back/Forward buttons clicked)

Brian Bishop

Dublin, Ireland
v7.2.15+sha.e7897e1