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:
- Hide the image - "background-image: url(''); opacity: 0; visibility: hidden"
- Asynchronously load the image
- 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
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
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. Thewindow.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)
Links
- Google Render-tree Construction, Layout, and Paint
- Google Developers Lazy Loading Images
- Medium Engineering Image Accessibility