MurhafSousli / ngx-gallery

Angular Gallery, Carousel and Lightbox
https://ngx-gallery.netlify.app/
MIT License
609 stars 129 forks source link

itemTemplate for Lightbox Gallery #600

Closed bcwhite-code closed 9 months ago

bcwhite-code commented 9 months ago

What is the expected behavior?

With v9, I had working code like this:

<ng-template #gallerySlide let-data="data">
    <span *ngIf="(data.caption??'')!=''" class="c-img-caption">
        {{data.caption}}
    </span>
</ng-template>

<ng-template #galleryBox let-state="state" let-config="config">
    <div class="c-btn c-btn-share-tab" title="Send image to another tab." i18n-title>
        <mat-icon class="c-btn-icon" (click)="onShareToTabClicked($event)">open_in_browser</mat-icon>
    </div>
    <div *ngIf="pvActive" class="c-btn c-btn-share-pv" title="Send image to player-view." i18n-title>
        <mat-icon class="c-btn-icon" (click)="onShareToPVClicked($event)">preview</mat-icon>
    </div>
</ng-template>

with

const gallery = this.gallery.ref(GALLERY_ID)
gallery.setConfig({
    itemTemplate: this.gallerySlide,
    boxTemplate: this.galleryBox,
    ...
})
...
this.lightbox.open(img_num, GALLERY_ID)

This worked and included my custom button (on the box) and custom data caption (on the item).

What is the current behavior?

In v11, though the boxTemplate of the config still works, the itemTemplate does not. My custom data caption does not appear.

Which versions are you using for the following packages?

Angular: 17.1.1 Angular CDK: 17.1.1 Angular CLI: 17.1.1 Typescript: v5.3.3 Gallery: 11.0.0

Is there anything else we should know?

The Gallery API says, under "available options in GalleryConfig":

So it seems it should be supported but I did notice that the lightbox template examples page does not include itemTemplate in the config.

Is there something I'm missing here?

MurhafSousli commented 9 months ago

The example does not show all the option, but it has all boxTemplate itemTemplate imageTemplate and thumbTemplate. in they belong to the gallery config.

Could you add a reproduction? also have thought about trying v12

bcwhite-code commented 9 months ago

So, itemTemplate in a galleryConfig added to a lightbox gallery (by ID) should work in v11 with that option just like it did in v9.

I'll see if I can narrow it down and get more information.

bcwhite-code commented 9 months ago

Working through this...

The documentation says:

The code, however, has:

<ng-container *ngIf="load" [ngSwitch]="type">
  <ng-container *ngSwitchCase="Types.Image">
    <gallery-image [src]="imageData.src"
                   [alt]="imageData.alt"
                   [index]="index"
                   [loadingAttr]="config.loadingAttr"
                   [loadingIcon]="config.loadingIcon"
                   [loadingError]="config.loadingError"
                   (error)="error.emit($event)"></gallery-image>

    <div *ngIf="config.imageTemplate" class="g-template g-item-template">
      <ng-container *ngTemplateOutlet="config.imageTemplate; context: imageContext"></ng-container>
    </div>
  </ng-container>

  <gallery-video *ngSwitchCase="Types.Video"
                 [src]="videoData.src"
                 [mute]="videoData.mute"
                 [poster]="videoData.poster"
                 [controls]="videoData.controls"
                 [controlsList]="videoData.controlsList"
                 [disablePictureInPicture]="videoData.disablePictureInPicture"
                 [play]="isAutoPlay"
                 [pause]="currIndex !== index"
                 (error)="error.emit($event)"></gallery-video>

  <gallery-iframe *ngSwitchCase="Types.Youtube"
                  [src]="youtubeSrc"
                  [autoplay]="isAutoPlay"
                  [loadingAttr]="config.loadingAttr"
                  [pause]="currIndex !== index"></gallery-iframe>

  <gallery-iframe *ngSwitchCase="Types.Iframe"
                  [src]="data.src"
                  [loadingAttr]="config.loadingAttr"></gallery-iframe>

  <ng-container *ngSwitchDefault>
    <div *ngIf="config.itemTemplate" class="g-template g-item-template">
      <ng-container *ngTemplateOutlet="config.itemTemplate; context: itemContext"></ng-container>
    </div>
  </ng-container>
</ng-container>

The ngIf for config.itemTemplate is inside a switch case of ngSwitchDefault meaning that it doesn't get emitted for "any type of item template" but instead only those that are not image, video, youtube, or iframe.

That should mean that if I instead use imageTemplate then my caption overlay should appear. It fails, however, because the let-data="data" is not working. The attempt to access data.caption throws an "undefined" exception.

The images are added to the gallery with this code:

gallery.addImage({
    src:   this.urlPrefix + parts[0] + this.urlFullPostfix,
    thumb: this.urlPrefix + parts[0] + this.urlThumbPostfix,
    caption: captionString,
} as ImageItemData)

What is the new method for accessing arbitrary data fields from within a template?

MurhafSousli commented 9 months ago

When you use itemTemplate, you are creating your own type, so you also have to define this type when creating new items https://github.com/MurhafSousli/ngx-gallery/wiki/Custom-Templates#galleryitemdef

Therefore, you should use the gallery.ref.add not addImage

galleryRef.add({
  type: 'my-image-template',
  data: {
    src: 'IMAGE_SRC_URL',
    thumb: 'IMAGE_THUMBNAIL_URL'
    alt: 'Test'
  }
})

and in template

<div *galleryItemDef="let item; let type = type">
  <div *ngIf="type === 'my-image-template'">
    <img [src]="item.src">
  </div>
  <div *ngIf="type === 'my-video-template'">
    <video>  
      <source src="item.src">
    </video>
   </div>
</div>
bcwhite-code commented 9 months ago

I see.

I don't want a custom item since I'm only displaying images so I'll just use imageTemplate and addImage().

This works as expected:

<ng-template #gallerySlide>
    <span class="c-img-caption">testing</span>
</ng-template>

But this doesn't, showing nothing:

<ng-template #gallerySlide>
    <span *galleryImageDef="let item; let active=active" class="c-img-caption">testing</span>
</ng-template>

As soon as I add *galleryImageDef to any element (span, div, ng-container, etc.), that element and below are not added to the DOM. There are no console messages.

MurhafSousli commented 9 months ago

You don't need to wrap them with ng-template, the following is sufficient:

 <span *galleryImageDef="let item; let active=active" class="c-img-caption">testing</span>

Basically using a star before the directive name is a shortcut to produce

<ng-template>
    <span [galleryImageDef]="let item; let active=active" class="c-img-caption">testing</span>
</ng-template>

When querying with ViewChild just use the directive class

@ViewChild(GalleryImageDef) imageDef: GalleryImageDef;
bcwhite-code commented 9 months ago

In my component's template, I tried:

...
<span *galleryImageDef="let item; let active=active" class="c-img-caption">testing</span>
...

but

@ViewChild(GalleryImageDef) imageDef: GalleryImageDef;

results in undefined. Even if it had been found, I can't assign that to imageTemplate in setConfig() because the types don't match: Type  GalleryImageDef  is missing the following properties from type  TemplateRef :  elementRef, createEmbeddedView

I tried the "expanded" version with [galleryImageDef] but that is an error: Property galleryImageDef is not provided by any applicable directives nor by span element

For reference, here is my entire component:

import {AfterViewInit, Component, Input, OnChanges, SimpleChanges, TemplateRef, ViewChild} from '@angular/core';
import {CommonModule} from "@angular/common";
import {Gallery, GalleryState, ImageItemData} from "ng-gallery";
import {Lightbox} from "ng-gallery/lightbox";
import {MatIconModule} from "@angular/material/icon";
import {MatTooltipModule} from "@angular/material/tooltip";

import {AppConstants} from "../AppConstants";
import {ImageThumbnailStripComponent} from "./image-thumbnail-strip/image-thumbnail-strip.component";

const GALLERY_ID = "thumbstrip-gallery"

@Component({
    standalone: true,
    imports: [CommonModule, ImageThumbnailStripComponent, MatIconModule, MatTooltipModule],
    selector: 'lightbox-thumbnail-strip',
    template: `
        <image-thumbnail-strip [urlPrefix]="urlPrefix" [urlPostfix]="urlThumbPostfix" [imageIds]="imageIds"
                               [orientation]="orientation">
        </image-thumbnail-strip>

        <ng-template #gallerySlide>
            <span *galleryImageDef="let item; let active=active" class="c-img-caption">testing</span>
            <!-- span *ngIf="(data.caption??'')!=''" class="c-img-caption">
                {{data.caption}}
            </span -->
        </ng-template>

        <ng-template #galleryBox>
            <div class="c-btn c-btn-share-tab" title="Send image to another tab." i18n-title>
                <mat-icon class="c-btn-icon" (click)="onShareToTabClicked($event)">open_in_browser</mat-icon>
            </div>
        </ng-template>
    `,
    styles: [`
        :host, lightbox-thumbnail-strip {
            display: block;
            height: 100%;
            width: 100%;
            cursor: pointer;
        }
        .c-img-caption {
            position: absolute;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%);
            padding: 0.4rem 1rem 0.4rem 1rem;
            background-color: antiquewhite;
            border: 1px solid black;
            color: black;
            border-radius: 1.0rem;
            font-size: 1.2rem;
            font-style: italic;
            font-weight: bold;
        }
        .c-btn {
            position: absolute;
            left: 0.9em;
            width: 2em;
            height: 2em;
            z-index: 60;
            color: white;
            opacity: 0.6;
            text-shadow: 0 0 2px rgba(0,0,0,0.8);
            transition: opacity linear 0.15s;
            cursor: pointer;
        }
        .c-btn-icon {
            width: 2em;
            height: 2em;
            font-size: 2em;
        }
        .c-btn:hover {
            opacity: 1;
        }
        .c-btn-share-tab {
            top: 0.9em;
            left: 0.9em;
        }
        .c-btn-share-pv {
            top: 3.9em;
            left: 0.9em;
        }
    `]
})
export class LightboxThumbnailStripComponent implements AfterViewInit, OnChanges {
    @Input() urlPrefix: string = ''
    @Input() urlFullPostfix: string = ''
    @Input() urlThumbPostfix: string = ''
    @Input() imageTags: string[] = []
    @Input() orientation: 'h'|'v' = 'h'

    @ViewChild(ImageThumbnailStripComponent) thumbnailStrip!: ImageThumbnailStripComponent
    @ViewChild('gallerySlide') gallerySlide!: TemplateRef<any>
    @ViewChild('galleryBox')   galleryBox!:   TemplateRef<any>

    imageIds: string[] = []
    currentIndex: number = 0

    constructor(private gallery: Gallery, private lightbox: Lightbox) {
    }

    ngAfterViewInit(): void {
        this.thumbnailStrip.clicked$.subscribe((id: string) => {
            this.openLightbox(id)
        })
        this.gallery.ref(GALLERY_ID).indexChanged.subscribe((state: GalleryState) => {
            this.currentIndex = state.currIndex || 0
        })
    }

    ngOnChanges(changes: SimpleChanges) {
        this.imageIds = this.imageTags.map(tag => tag.split(':', 1)[0])
    }

    openLightbox(id: string) {
        const gallery = this.gallery.ref(GALLERY_ID)
        gallery.reset()
        gallery.setConfig({
            imageTemplate: this.gallerySlide,
            boxTemplate: this.galleryBox,
            thumb: this.imageTags.length > 1,

            loadingStrategy: "lazy",
            slidingDirection: "horizontal",
            loop: false,
            counterPosition: "top",
            nav: true,
            dots: false,
            autoPlay: false,
        })
        this.lightbox.setConfig({
            hasBackdrop: true,
            keyboardShortcuts: true,
            panelClass: 'fullscreen',
        })
        for (let tag of this.imageTags) {
            let parts = tag.split(':')
            if (parts.length == 1) parts[1] = parts.slice(1).join(':')
            gallery.addImage({
                src:   this.urlPrefix + parts[0] + this.urlFullPostfix,
                thumb: this.urlPrefix + parts[0] + this.urlThumbPostfix,
                caption: (parts.length > 1) ? parts[1] : '',
            } as ImageItemData)
        }

        for (let i = 0; i < this.imageIds.length; i++) {
            if (this.imageIds[i].startsWith(id)) {
                this.currentIndex = i
                this.lightbox.open(i, GALLERY_ID)
                break
            }
        }
    }

    onShareToTabClicked($event: MouseEvent) {
        let parts = this.imageTags[this.currentIndex].split(':')
        if (parts.length == 1) parts[1] = parts.slice(1).join(':')
        window.open(`${AppConstants.SHOW_PAGE}?b=${parts[0]+this.urlFullPostfix}&c=${encodeURIComponent(parts[1])}`, 'dimg')
    }
}
bcwhite-code commented 9 months ago

Ahhh... This works:

<ng-template #gallerySlide let-data>
    <span class="c-img-caption" *ngIf="(data.caption??'')!=''">{{data.caption}}</span>
</ng-template>
MurhafSousli commented 9 months ago

Ofcourse, this code will not work

<ng-template  #gallerySlide>
  <span *galleryImageDef="let item; let active=active" class="c-img-caption">testing</span>
</ng-template>

Because wrapping content with ng-template means it will not be rendered, therefore @ViewChild will return undefined

This is how you should use it

@Component({
    standalone: true,
    imports: [GalleryModule, CommonModule, ImageThumbnailStripComponent, MatIconModule, MatTooltipModule],
    selector: 'lightbox-thumbnail-strip',
    template: `
        <image-thumbnail-strip [urlPrefix]="urlPrefix" [urlPostfix]="urlThumbPostfix" [imageIds]="imageIds"
                               [orientation]="orientation">
        </image-thumbnail-strip>

       <span *galleryImageDef="let item; let active=active" class="c-img-caption">testing</span>

        <div *galleryBoxDef class="c-btn c-btn-share-tab" title="Send image to another tab." i18n-title>
                <mat-icon class="c-btn-icon" (click)="onShareToTabClicked($event)">open_in_browser</mat-icon>
        </div>
    `,
})
export class LightboxThumbnailStripComponent implements AfterViewInit, OnChanges {

    @ViewChild(GalleryImageDef) imageDef: GalleryImageDef;

    @ViewChild(GalleryBoxDef) galleryBox: GalleryBoxDef;

    ngAfterViewInit(): void {
        this.thumbnailStrip.clicked$.subscribe((id: string) => {
            this.openLightbox(id)
        })

        // Set the config here, you can use setConfig or inside ref function
        this.gallery.ref(GALLERY_ID, {
            imageTemplate: this.imageDef.templateRef,
            boxTemplate: this.galleryBox.templateRef,
            loadingStrategy: "lazy",
            slidingDirection: "horizontal",
            loop: false,
            counterPosition: "top",
            nav: true,
            dots: false,
            autoPlay: false,
        }).indexChanged.subscribe((state: GalleryState) => {
            this.currentIndex = state.currIndex || 0
        })
    }

    openLightbox(id: string) {
        const gallery = this.gallery.ref(GALLERY_ID)
        gallery.reset()
       // Not sure if you need to do it here but you can update the config at any time
        gallery.setConfig({
            thumb: this.imageTags.length > 1,
        })
        this.lightbox.setConfig({
            hasBackdrop: true,
            keyboardShortcuts: true,
            panelClass: 'fullscreen',
        })
       // .....
   }
}

Don't forget to import GalleryModule or its directives individually in the component's imports

bcwhite-code commented 9 months ago

As I said, I tried just

<span *galleryImageDef="let item; let active=active" class="c-img-caption">testing</span>

(no surrounding ) and it wasn't found. Maybe because there was some import missing but there were no build errors. In addition, I could not pass a variable of the form

@ViewChild(GalleryImageDef) imageDef: GalleryImageDef;

to imageTemplate because of a type mismatch.

But it does work with just

<ng-template #gallerySlide let-data>
    <span class="c-img-caption" *ngIf="(data.caption??'')!=''">{{data.caption}}</span>
</ng-template>
MurhafSousli commented 9 months ago

I just tried it on my end, it works... They all have the same type when you pass the imageDef.templateRef, maybe you are passing just imageDef and thus you get type mismatch

If you provide a reproduction stackblitz I can help with it, but if you are good with ng-template then we can close this I guess

bcwhite-code commented 9 months ago

imageDef.templateRef would be the difference.

Thanks for all your help!