upp-image
A versatile image component with built-in cropping, lazy loading, EXIF orientation handling, and base64 conversion. It integrates with Angular reactive forms as a ControlValueAccessor, supporting both readonly display and editable modes where users can select and crop images through a modal interface.
When to Use
- You need to display an image that may come from a URL or a base64 string.
- You want to let users pick and crop an image before uploading (editable mode).
- You need an image input that works inside Angular reactive forms.
- You need automatic EXIF orientation correction for user-uploaded photos.
- You want lazy loading and preload/cache integration for server-hosted images.
Demo
Source Code
- HTML
- TypeScript
- SCSS
<div class="demo-scroll-container">
<upp-scrollable [scrollbar]="'y'">
<div class="demo-content">
<h2>upp-image</h2>
<p class="demo-description">Product photo display & editing — toggle the lock to switch between readonly and editable modes.</p>
<!-- Controls -->
<div class="demo-controls">
<ion-button size="small" (click)="toggleReadonly()">
<ion-icon name="lock-closed-outline" slot="start" *ngIf="isReadonly"></ion-icon>
<ion-icon name="lock-open-outline" slot="start" *ngIf="!isReadonly"></ion-icon>
Editable: {{ !isReadonly }}
</ion-button>
<div class="demo-control-group">
<label class="demo-label">Crop width</label>
<input type="number" [(ngModel)]="crwidth" class="demo-number-input">
</div>
<div class="demo-control-group">
<label class="demo-label">Crop height</label>
<input type="number" [(ngModel)]="crheight" class="demo-number-input">
</div>
</div>
<!-- Updated banner -->
<div class="demo-toast" *ngIf="imageUpdated">
<ion-icon name="checkmark-circle-outline"></ion-icon>
Image updated!
</div>
<!-- Single image: toggles readonly/editable -->
<div class="demo-section">
<h3>Product Photo ({{ isReadonly ? 'readonly' : 'editable' }})</h3>
<p class="demo-label">{{ isReadonly ? 'Click the lock to enable editing.' : 'Change/Delete overlay the image.' }}</p>
<div class="image-frame">
<upp-image #imageRef
[src]="imageSrc"
[readonly]="isReadonly"
[crwidth]="crwidth"
[crheight]="crheight"
(Changed)="onImageChanged(imageRef)">
</upp-image>
</div>
</div>
</div>
</upp-scrollable>
</div>
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { UppImageComponent } from '@unpispas/upp-wdgt';
@Component({
selector: 'demo-upp-image',
templateUrl: './demo-upp-image.html',
styleUrls: ['../demo-common.scss', './demo-upp-image.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DemoUppImageComponent {
isReadonly = true;
crwidth = '200';
crheight = '200';
imageUpdated = false;
/** Synced with upp-image value so delete clears the displayed image and Change stays visible. */
imageSrc: string | null = 'https://picsum.photos/seed/product/300/300';
constructor(private change: ChangeDetectorRef) {
}
toggleReadonly() {
this.isReadonly = !this.isReadonly;
this.change.markForCheck();
}
onImageChanged(imageRef: UppImageComponent) {
this.imageSrc = imageRef.value;
this.imageUpdated = true;
this.change.markForCheck();
setTimeout(() => {
this.imageUpdated = false;
this.change.markForCheck();
}, 2000);
}
}
:host {
display: block;
height: 100vh;
}
.demo-scroll-container {
height: 100%;
}
.demo-content {
padding: 12px;
}
.image-frame {
width: 200px;
height: 200px;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--ion-color-light-shade, #ddd);
}
.demo-toast {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
margin-bottom: 16px;
background: var(--ion-color-success-tint, #d4edda);
color: var(--ion-color-success-shade, #155724);
border-radius: 6px;
font-size: 13px;
font-weight: 500;
ion-icon {
font-size: 18px;
}
}
.demo-control-group {
display: flex;
align-items: center;
gap: 6px;
}
.demo-number-input {
width: 64px;
padding: 4px 8px;
border: 1px solid var(--ion-color-medium-tint, #ccc);
border-radius: 4px;
font-size: 13px;
text-align: center;
}
API Reference
upp-image
Selector: upp-image
Implements: ControlValueAccessor, OnInit, OnChanges, OnDestroy
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
readonly | boolean | true | When true, the image is display-only. When false, the user can select a file and crop the image. |
src | string | null | null | Image source. Can be a URL or a base64 string. |
crheight | string | '0' | Crop height constraint in pixels (passed to the crop modal). |
crwidth | string | '0' | Crop width constraint in pixels (passed to the crop modal). |
formControlName | string | '' | Name of the reactive form control this component is bound to. |
value | string | null | (derived) | The current image value (base64 or URL). Supports two-way binding via ControlValueAccessor. |
Outputs
| Name | Type | Description |
|---|---|---|
Changed | EventEmitter<any> | Emits whenever the image is modified (file selected, cropped, or cleared). |
Computed Properties
| Property | Type | Description |
|---|---|---|
IsCached | boolean | Whether the current src is available in the preload cache. |
IsVisible | boolean | true if the image has loaded or is cached. |
Base64 | Promise<string | null> | Asynchronously returns the base64-encoded PNG representation of the current image. |
upp-modal-crop-image
Selector: upp-modal-crop-image
Internal modal component used by upp-image for cropping. Not intended for direct use.
| Name | Type | Description |
|---|---|---|
@Input() image | any | The image element to crop. |
@Input() height | number | Desired crop height. |
@Input() width | number | Desired crop width. |