upp-visible-control
A wrapper component that uses the VisibleDirective to detect when its content enters or leaves the viewport. It automatically detaches Angular change detection while the component is hidden and reattaches it when visible, providing a significant performance optimization for lists with many items.
When to Use
- You have a scrollable container with many items and want to avoid unnecessary change detection cycles for off-screen elements.
- You need to track which items are currently visible in a scroll container (e.g., analytics, lazy rendering, virtual scroll alternatives).
- You want to optimize rendering performance by pausing updates on hidden content.
Demo
Source Code
- HTML
- TypeScript
- SCSS
<h2>upp-visible-control</h2>
<p class="demo-description">Lazy image gallery — content renders only when <code>upp-visible-control</code> detects the item is in the viewport.</p>
<div class="demo-controls">
<ion-badge color="primary" class="counter-badge">
{{ visibleCount }} of {{ images.length }} images in viewport
</ion-badge>
<span class="hint">Scroll down to load more</span>
</div>
<div class="demo-section">
<div class="gallery-scroll">
<div *ngFor="let img of images; let i = index" class="gallery-slot">
<upp-visible-control (visibilityChange)="onVisibilityChange(i, $event)">
<div class="image-card" *ngIf="visibilityMap[i]; else placeholder">
<div class="image-preview" [style.background]="img.color">
<ion-icon [name]="img.icon" class="image-icon"></ion-icon>
</div>
<div class="image-info">
<span class="image-title">{{ img.title }}</span>
<span class="image-desc">{{ img.description }}</span>
</div>
</div>
<ng-template #placeholder>
<div class="image-card image-card--placeholder">
<div class="image-preview image-preview--placeholder">
<ion-icon name="image-outline" class="image-icon image-icon--placeholder"></ion-icon>
</div>
<div class="image-info">
<span class="placeholder-bar placeholder-bar--title"></span>
<span class="placeholder-bar placeholder-bar--desc"></span>
</div>
</div>
</ng-template>
</upp-visible-control>
</div>
</div>
</div>
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
interface GalleryImage {
id: number;
title: string;
description: string;
color: string;
icon: string;
}
@Component({
selector: 'demo-upp-visible-control',
templateUrl: './demo-upp-visible-control.html',
styleUrls: ['../demo-common.scss', './demo-upp-visible-control.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DemoUppVisibleControlComponent {
images: GalleryImage[] = [
{ id: 1, title: 'Mountain Sunrise', description: 'Golden light over snowy peaks', color: '#e8a87c', icon: 'sunny-outline' },
{ id: 2, title: 'Ocean Waves', description: 'Crashing waves at sunset', color: '#85cdca', icon: 'water-outline' },
{ id: 3, title: 'Forest Path', description: 'Mossy trail through old-growth woods', color: '#7bc67e', icon: 'leaf-outline' },
{ id: 4, title: 'City Skyline', description: 'Neon lights reflecting on glass', color: '#6c7b95', icon: 'business-outline' },
{ id: 5, title: 'Desert Dunes', description: 'Windswept sand under a clear sky', color: '#d4a76a', icon: 'partly-sunny-outline' },
{ id: 6, title: 'Northern Lights', description: 'Aurora dancing over a frozen lake', color: '#7f8fa6', icon: 'planet-outline' },
{ id: 7, title: 'Cherry Blossoms', description: 'Pink petals lining a quiet street', color: '#e6a1b0', icon: 'flower-outline' },
{ id: 8, title: 'Waterfall', description: 'Cascading water in a tropical jungle', color: '#63b3ed', icon: 'water-outline' },
{ id: 9, title: 'Starry Night', description: 'Milky way visible from a hilltop', color: '#4a5568', icon: 'moon-outline' },
{ id: 10, title: 'Autumn Leaves', description: 'Red and orange canopy over a park', color: '#c0765a', icon: 'leaf-outline' },
{ id: 11, title: 'Coral Reef', description: 'Vibrant marine life underwater', color: '#48c9b0', icon: 'fish-outline' },
{ id: 12, title: 'Lavender Field', description: 'Purple rows stretching to the horizon', color: '#9b7fbd', icon: 'flower-outline' },
{ id: 13, title: 'Glacial Lake', description: 'Turquoise water surrounded by ice', color: '#74b9ff', icon: 'snow-outline' },
{ id: 14, title: 'Volcanic Crater', description: 'Steaming crater at dawn', color: '#a0522d', icon: 'flame-outline' },
{ id: 15, title: 'Bamboo Grove', description: 'Tall bamboo swaying in the breeze', color: '#81b29a', icon: 'leaf-outline' },
];
visibilityMap: boolean[] = new Array(15).fill(false);
visibleCount = 0;
constructor(private change: ChangeDetectorRef) {}
onVisibilityChange(index: number, isVisible: boolean) {
this.visibilityMap[index] = isVisible;
this.visibleCount = this.visibilityMap.filter(v => v).length;
this.change.markForCheck();
}
}
:host {
display: block;
padding: 16px;
}
.counter-badge {
font-size: 14px;
padding: 8px 14px;
}
.hint {
font-size: 12px;
color: var(--ion-color-medium, #888);
display: flex;
align-items: center;
}
.gallery-scroll {
height: 300px;
overflow-y: auto;
border: 1px solid var(--ion-color-light-shade, #d7d8da);
border-radius: 10px;
padding: 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
.gallery-slot {
flex-shrink: 0;
}
.image-card {
display: flex;
gap: 12px;
padding: 10px;
border-radius: 8px;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
transition: opacity 0.2s ease;
}
.image-card--placeholder {
opacity: 0.5;
}
.image-preview {
width: 64px;
height: 64px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.image-preview--placeholder {
background: var(--ion-color-light-shade, #d7d8da);
}
.image-icon {
font-size: 28px;
color: #fff;
}
.image-icon--placeholder {
color: var(--ion-color-medium, #888);
}
.image-info {
display: flex;
flex-direction: column;
justify-content: center;
gap: 3px;
min-width: 0;
}
.image-title {
font-weight: 600;
font-size: 14px;
color: var(--ion-color-dark, #333);
}
.image-desc {
font-size: 12px;
color: var(--ion-color-medium, #888);
}
.placeholder-bar {
display: block;
border-radius: 4px;
background: var(--ion-color-light-shade, #d7d8da);
}
.placeholder-bar--title {
width: 120px;
height: 14px;
}
.placeholder-bar--desc {
width: 180px;
height: 11px;
}
API Reference
upp-visible-control
Selector: upp-visible-control
| Type | Name | Description |
|---|---|---|
@Output() | visibilityChange: EventEmitter<boolean> | Emits true when the component enters the viewport and false when it leaves. |
Behavior:
- On initialization (
ngAfterViewInit), if the component is not visible, change detection is detached immediately. - When the component becomes hidden, change detection is detached to save rendering cycles.
- When the component becomes visible again, change detection is reattached and
detectChanges()is called to refresh the view.