0

I created an attribute directive ImageSpinloaderDirective to conditionally change the appearance of the user's profile picture, depending on wether there is a picture and if it is already loaded:

  • If the src attribute's URL can't be resolved (404, etc), a default image should be shown
  • If the picture is still loading a loading animation should be shown
  • If the user has changed, a new profile image is loaded and the spinloader should appear again until loading is finished

My approach is to listen to the DOM-events load and error in the ImageSpinloaderDirective and to set the src attribute and img-loading class of the <img> element accordingly. The img-loading class sets the images opacitiy to 0 and uses an animated gif as background image.

Now I wanted to change the directive's isLoading field from the parent component (UserDetailsComponent) every time a different user is displayed. I thought that using the @ViewChild annotation within that component would be the right choice to accomplish that. However the binding seems to fail for some reason, since UserDetailsComponent's imageSpinloaderDirective is always undefined.

Is it possible at all to use attribute directives with the @ViewChild annotation? Is there any better/alternative way to bind an attribute directive to a component? Or is this approach totally wrong and should be implemented in a different way?

user-details.component.ts

Component({
    moduleId: module.id,
    selector: 'user-details',
    templateUrl: 'user-details.html',
    styleUrls: [
        './../grid/grid-item-details.css',
        './../grid/grid.css'
    ],
    animations: [fadeInOut]
})
export class UserDetailsComponent extends GridItemDetailsComponent<User>
{
    public user: User;
    public profileImageUrl: string;

    @ViewChild(ImageSpinloaderDirective)
    public imageSpinloaderDirective: ImageSpinloaderDirective;

    public loadDetails(item: User): void
    {
        this.user = item;
        this.imageSpinloaderDirective.isLoading = true;
        this.profileImageUrl = `${this.configService.config.profileApiUrl}/image/${item.id}`;
    }
}

user-details.html

<div class="row">
  <div class="col-lg-12">
    <div class="img-loader">
      <img id="foo" imgspinloader
           class="img-rounded"
           src="{{profileImageUrl}}">
    </div>
  </div>
</div>

image-spinloader.directive.ts

import
{
    AfterContentChecked,
    Directive,
    HostBinding,
    HostListener,
    Input,
    OnInit
} from '@angular/core';

@Directive({
    selector: '[imgspinloader]'
})
export class ImageSpinloaderDirective
{
    private defaultSrc: string = "assets/img/user-image-invitation-2x.png";

    @Input() @HostBinding() src;

    @Input() @HostBinding("class.img-loading") isLoading: boolean;

    @HostListener('load') complete()
    {
        this.isLoading = false;
    }

    @HostListener('error') onError()
    {
        this.src = this.defaultSrc;
        this.isLoading = false;
    }
}
wodzu
  • 3,004
  • 3
  • 25
  • 41
  • 1
    Apparently it should work. Are you sure you have added your directive in @NgModule? Here is my investigation https://stackblitz.com/edit/angular-pw7sj3?file=app%2Fapp.component.ts – yurzui Nov 14 '17 at 12:13
  • @yurzui yes,I've added `ImageSpinloaderDirective` right next to `UserDetailsComponent` in the `declarations` array of my `UserModule`. The directive itself is also working. – wodzu Nov 14 '17 at 12:29
  • Can you reproduce the issue on stackblitz? – yurzui Nov 14 '17 at 12:29
  • I've added a wrapping container element to the template with an `*ngIf` like in my app. It seems to break the binding process. [See here](https://stackblitz.com/edit/angular-gu55nb?embed=1&file=app/app.component.html) – wodzu Nov 14 '17 at 12:48
  • Placing your control inside EmbeddedView changes behavior. You can't access it within ngOnInit in this case because you deal with dynamic query – yurzui Nov 14 '17 at 12:50
  • In this case there is ngAfterViewInitHook or you can use setter, or subcribe to `@ViewChildren` changes – yurzui Nov 14 '17 at 12:52
  • Well, but in my original problem, i'm not using any lifecycle hook to access the directive. I've just used the `ngOninit` because you did in the initial sample. In my app i'm just using the `loadDetails()` to access the directive, see [this updated sample](https://stackblitz.com/edit/angular-gu55nb?embed=1&file=app/app.component.html). The toggling the template via `*ngIf` entirely dereferences the directive from the `@ViewChild` binding. Is there a way to prevent this, except for hiding the entire section via css? – wodzu Nov 14 '17 at 13:19
  • When user is false your directive is destroyed. So you can't access any property from it. If you want to keep it alive you can use hidden property. But it similar css – yurzui Nov 14 '17 at 13:32
  • What about default value for property? `@Input() @HostBinding("class.img-loading") isLoading: boolean = true;` It will stay true until load or error event happens – yurzui Nov 14 '17 at 13:35
  • And you forgot about `complete` property on image in your implementation – yurzui Nov 14 '17 at 13:40
  • Revise it here https://stackoverflow.com/questions/280049/javascript-callback-for-knowing-when-an-image-is-loaded – yurzui Nov 14 '17 at 13:40
  • unfortunately `isLoading` must be reset to `true`, when the `src` attribute of `` is changed and i'm afraid listening to changes of the directive's property can run into conflict with the DOM events `load` or `error` if they return before `isLoading` is set in the component. this would result with an infinite loading animation. – wodzu Nov 14 '17 at 13:41
  • Can you reproduce this problem? – yurzui Nov 14 '17 at 13:43
  • I tried to use the `complete` property before. But angular does not know it, I always get error messages like: "Can't bind to 'complete' since it isn't a known property of 'img'" I used syntax like: `@HostBinding('complete') cpl: boolean;` or `@HostBinding('attr.complete') cpl: boolean;` – wodzu Nov 14 '17 at 13:52
  • You can try `constructor(private elRef: ElementRef) if (this.elRef.nativeElement.complete)`. Anyway, i think the problem here is something other. I don't understand how hide/show button relates with changing src attribute – yurzui Nov 14 '17 at 13:58
  • thanks anyway, you helped me to find the main issue – wodzu Nov 14 '17 at 14:01

0 Answers0