Search code examples
angularangular-directive

Angular 17 custom Directive rendering don't work


I'm learning Angular.

I want to create custom directive, but doesn't work ! (it's not app for production, it's a discover app ^^)

I have no error, the project build ! But the directive don't work !

To create the directive, I use : ng generate directive directive-name

Can you help me to resolve my problem ?

The code :

Component - app.component.ts

import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { POKEMONS } from './mock-pokemon-list';
import { Pokemon } from './pokemon';
import { AppModule } from './app.module';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RouterOutlet,
    AppModule
  ],
  templateUrl: 'app.component.html',
  styles: [],
})
export class AppComponent implements OnInit {
  pokemonList: Pokemon[] = POKEMONS;
  pokemonSelected: Pokemon|undefined;

  ngOnInit(): void {
      console.log(this.pokemonList);
  }

  selectPokemon(pokemonId: string) {
    const id: number = +pokemonId;
    const pokemon: Pokemon|undefined = this.pokemonList.find(pokemon => pokemon.id == id);
    if(pokemon) {
      console.log(`Vous avez cliqué sur le pokémon ${pokemon.name}`);
      this.pokemonSelected = pokemon;
    } else {
      console.log(`Vous avez demandé un pokémon qui n'existe pas.`);
      this.pokemonSelected = pokemon;
    }
  }
}

Template - app.component.html

<h1 class="center" >Liste de pokémons</h1>
<input
    #input
    (keyup.enter)="selectPokemon(input.value)"
    type="number"
/>
@if(pokemonSelected) {
    <p>Vous avez sélectionné le pokémon : {{ pokemonSelected.name }}</p>
}
@else {
    <p>Aucune correspondance.</p>
}
<div class="container" >
    <div class="row" >
@for (pokemon of pokemonList; track pokemon) {
    <div class="col s12 m4 s6">
        <div class="card horizontal" appBorderCard>
          <div class="card-image">
            <img [src]="pokemon.picture">
          </div>
          <div class="card-stacked">
            <div class="card-content">
                <p class="header">{{ pokemon.name }}</p>
                <p><small>{{ pokemon.created }}</small></p>
            </div>
          </div>
        </div>
      </div>
} @empty {
    <p>Aucun élément dans la base.</p>
}
    </div>
</div>
<router-outlet />

The directive - border-card.directive.ts

import { Directive, ElementRef, HostListener } from '@angular/core';

@Directive({
  selector: '[appBorderCard]'
})
export class BorderCardDirective {

  constructor(private el: ElementRef) {
    this.setHeight(180);
    this.setBorder("#f5f5f5");
  }

  @HostListener('mouseenter') onMouseEnter() {
    this.setBorder("#009688");
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.setBorder("#f5f5f5");
  }

  private setHeight(height: number) {
    this.el.nativeElement.style.height = `${height}px`;
  }

  private setBorder(color: string) {
    this.el.nativeElement.style.border = `solid 4px ${color}`;
  }

}

Module - app.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BorderCardDirective } from './border-card.directive';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    BorderCardDirective
  ]
})
export class AppModule { }

The stack :

Angular CLI: 17.3.2
Node: 21.6.2 (Unsupported)
Package Manager: npm 10.5.0

Angular: 
... 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1703.2 (cli-only)
@angular-devkit/core         17.3.2 (cli-only)
@angular-devkit/schematics   17.3.2 (cli-only)
@schematics/angular          17.3.2 (cli-only)

Thanks for advance


Solution

  • Approach 1:

    1. Please set the directive to be standalone: true

    directive

    import { Directive, ElementRef, HostListener } from '@angular/core';
    
    @Directive({
      selector: '[appBorderCard]',
      standalone: true,
    })
    export class BorderCardDirective {
    ...
    
    1. Add the directive to the imports array of the app.component.ts

    app.component

    import { Component, OnInit } from '@angular/core';
    import { RouterOutlet } from '@angular/router';
    import { POKEMONS } from './mock-pokemon-list';
    import { Pokemon } from './pokemon';
    import { AppModule } from './app.module';
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [
        RouterOutlet,
        AppModule,
        BorderCardDirective,
      ],
      templateUrl: 'app.component.html',
      styles: [],
    })
    export class AppComponent implements OnInit {
    ...
    

    Approach 2:

    1. Ensure that the directive is not set to standalone: true

      import { Directive, ElementRef, HostListener } from '@angular/core';

      @Directive({ selector: '[appBorderCard]', }) export class BorderCardDirective { ...

    2. Add the directive to the declarations and exports array of the app.module.ts, only if we export the directive, it will be visible to app.component.ts

    app.module.ts

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { BorderCardDirective } from './border-card.directive';
    
    @NgModule({
      declarations: [
        BorderCardDirective,
      ],
      imports: [
        CommonModule,
      ],
      exports: [
        BorderCardDirective,
      ],
    })
    export class AppModule { }
    

    If you are using standalone app component, please ensure you have imported the directive on the app.components imports array! Since we are adding to the imports array, we need to set standalone: true on the @Directive({selector: 'some selector', standalone: true }) object of the directive file:

    imports: [
    ...
    BorderCardDirective,
    ...
    ]
    

    If you are using modular approach, also same thing ensure app.module.ts has the directive added to the declarations array if it's not standalone, or to the imports array if it's standalone!

    If not imported then Angular will take appBorderCard as an HTML attribute and will not throw an error!

    So, if the component/directive is standalone and used in a module, it belongs on the imports array, if not it belongs on the declarations array.


    When using the standalone approach, we are limited to using only standalone true component/directive with them being added to the imports array!