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 readonly display plus two editable interaction modes: direct upload (upload) and inline configuration (config).
When to Use
- You need to display an image that may come from a URL or a base64 string.
- You want the previous direct-upload flow in editable mode (
mode="upload", default). - You want users to configure image content (local upload, avatar, or internet search) from an inline settings panel (
mode="config"). - 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. In <strong>upload</strong> mode it opens the file picker directly; in <strong>config</strong> mode it opens avatar/upload/search options plus crop on the current image when available.</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">Mode</label>
<div class="demo-mode-buttons">
<ion-button size="small" [fill]="selectedMode === 'upload' ? 'solid' : 'outline'" (click)="setMode('upload')">upload</ion-button>
<ion-button size="small" [fill]="selectedMode === 'config' ? 'solid' : 'outline'" (click)="setMode('config')">config</ion-button>
</div>
</div>
<div class="demo-control-group">
<label class="demo-label">Search seed</label>
<input type="text" [(ngModel)]="searchText" class="demo-text-input" maxlength="16">
</div>
<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' }}, mode: {{ selectedMode }})</h3>
<p class="demo-label">
{{ isReadonly ? 'Click the lock to enable editing.' : (selectedMode === 'config'
? 'Use settings for avatar, upload, search, or crop the current image.'
: 'Use camera icon to upload a local image directly.') }}
</p>
<div class="image-frame">
<upp-image #imageRef
[src]="imageSrc"
[readonly]="isReadonly"
[mode]="selectedMode"
[search]="searchText"
[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;
selectedMode: 'upload' | 'config' = 'config';
searchText = 'pizza margarita';
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();
}
setMode(mode: 'upload' | 'config') {
this.selectedMode = mode;
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;
}
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-bottom: 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-mode-buttons {
display: flex;
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;
}
.demo-text-input {
width: 150px;
padding: 4px 8px;
border: 1px solid var(--ion-color-medium-tint, #ccc);
border-radius: 4px;
font-size: 13px;
}
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 image action icon is enabled. |
mode | 'upload' | 'config' | 'upload' | Editable interaction mode. upload opens the OS file picker directly (legacy behavior). config opens the inline settings panel. |
search | string | null | null | Seed text copied into the config panel. Used by avatar generation and internet image search when opening mode="config". |
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. |
Configuration Mode Options
When mode="config" and the widget is editable:
- Avatar (
/photo/avatar): builds an avatar URL from text + configurable hex colors. - Color selection UI: uses the reusable
upp-colorwidget for background/text color selection. - Upload: opens local file picker, then crop modal.
- Search (
/photo/search): requests an internet product image and enables Use Found Image only when a valid result is returned.
Both backend routes are exposed under the server /photo group.
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. |