Skip to content

Latest commit

 

History

History
430 lines (307 loc) · 9.78 KB

File metadata and controls

430 lines (307 loc) · 9.78 KB

Exercise: Attribute Directives

In this exercise we want to build our first Attribute Directive.

Advanced way

implement the attribute directive TiltDirective (in src/app/shared/). The directive should rotate its host element (hint: ElementRef) when entering it with the mouse and reset the rotation when the mouse leaves the host element.

In addition to a simple rotation, the directive should rotate the element according to the position the cursor entered the element. If the cursor enters from left => rotate to the right and vice versa.

Use ElementRef#nativeElement#addEventListener to listen to events and nativeElement#style#transform to change the tilt degree of the dom element.

As a final step, make the tilt degrees configurable with an input binding.

Helper for advanced way
ng g directive shared/tilt
transform = 'rotate()';

this.elementRef.nativeElement.addEventListener('event', callbackFn);

/**
 *
 * returns 0 if entered from left, 1 if entered from right
 */
determineDirection(pos: number): 0 | 1 {
    const width = this.elementRef.nativeElement.clientWidth;
    const middle = this.elementRef.nativeElement.getBoundingClientRect().left + width / 2;
    return (pos > middle ? 1 : 0);
}

Step by Step

1. implement tilt directive

We are going to implement the attribute directive TiltDirective (in src/app/shared/). The directive should rotate its host element (hint: ElementRef) when entering it with the mouse and reset the rotation when the mouse leaves the host element.

generate the directive in src/app/shared/tilt.directive.ts

Generate TiltDirective
ng generate directive shared/tilt

OR

ng g d shared/tilt
// src/app/shared/tilt.directive.ts
import { Directive } from '@angular/core';

@Directive({
    selector: '[tilt]',
    standalone: true
})
export class TiltDirective {
    
    constructor() {}
}

Implement the OnInit Interface, the ngOnInit Lifecycle hook and inject the ElementRef in the constructor.

Tip

type the ElementRef as ElementRef<HTMLElement>, you will have an easier life

Inject ElementRef and implement OnInit
// src/app/shared/tilt.directive.ts

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

@Directive({
    selector: '[tilt]',
})
export class TiltDirective implements OnInit {
    
    constructor(private element: ElementRef<HTMLElement>) {}
    
    ngOnInit() {}
    
}

Setup the eventListeners for mouseleave and mouseenter in ngOnInit.

EventListener Setup
// src/app/shared/tilt.directive.ts

ngOnInit() {
  this.element.nativeElement.addEventListener('mouseleave', () => {
    // we want to reset the styles here
  });
  
  this.element.nativeElement.addEventListener('mouseenter', (event) => {
    // we want to set the styles here
  });
}

As for the callbacks, we want to set the nativeElement.style.transform value to either rotate(0deg) on reset or rotate(10deg) on mouse enter.

EventListener callbacks
// src/app/shared/tilt.directive.ts

ngOnInit() {
  this.element.nativeElement.addEventListener('mouseleave', () => {
    this.element.nativeElement.style.transform = 'rotate(0deg)';
  });

  this.element.nativeElement.addEventListener('mouseenter', () => {
    this.element.nativeElement.style.transform = 'rotate(5deg)';
  });
}

2. use directive to adjust behavior of movie-card

apply the tilt directive to the movie-card.component.ts template.

It should be applied to the div.movie-card.

Use the TiltDirective in MovieCardComponent
<!--movie-card.component.ts-->

<div class="movie-card" tilt>
    <!--  content-->
</div>

If not autocompleted, don't forget to add the TiltDirective to the MovieCardComponents import array.

// movie-card.component.ts

import { TiltDirective } from '../../shared/tilt.directive';

@Component({
  /**/,
  imports: [/*...*/, TiltDirective]
})
export class MovieCardComponent {}

serve the application and see if the tilt directive is applied and does what it should

ng serve

3. implement the funk :-D

now we want to add a more complex animation and tilt the movie-card according to the mouseposition on enter.

Create a method determineDirection(pos: number): 0 | 1 in the TiltDirective class, which returns 0 in case the mouse entered from the left side and 1 if it entered from the right side.

Use this method in the mouseenter callback in order to determine if we should tilt -15 or 15 degrees.

determineDirection
// tilt.directive.ts

ngOnInit() {
  this.element.nativeElement.addEventListener('mouseleave', () => {
    this.element.nativeElement.style.transform = 'rotate(0deg)';
  });

  this.element.nativeElement.addEventListener('mouseenter', event => {
    const pos = this.determineDirection(event.pageX);
    this.element.nativeElement.style.transform = `rotate(${pos === 0 ? '5deg' : '-5deg'})`;
  });
}

/**
 *
 * returns 0 if entered from left, 1 if entered from right
 */
determineDirection(pos: number): 0 | 1 {
  const width = this.element.nativeElement.clientWidth;
  const middle =
    this.element.nativeElement.getBoundingClientRect().left + width / 2;
  return pos > middle ? 1 : 0;
}

Very nice job! Take a look at the outcome. The behavior should be as described.

4. make tilt degrees configurable

We can also make the tilt degrees configurable by using an input binding in the TiltDirective.

configurable tilt degree
// src/app/shared/tilt.directive.ts

tiltDegree = input(5);

use the input value in the mouseenter callback.

use the tiltDegree value
// tilt.directive.ts

ngOnInit() {
  this.element.nativeElement.addEventListener('mouseleave', () => {
    this.element.nativeElement.style.transform = 'rotate(0deg)';
  });

  this.element.nativeElement.addEventListener('mouseenter', event => {
    const pos = this.determineDirection(event.pageX);
    this.element.nativeElement.style.transform = `rotate(${pos === 0 ? `${this.tiltDegree()}deg` : `-${this.tiltDegree()}deg`})`;
  });
}

4. Configure the degree in MovieCard

configure different tilt values in movie-card:

([tiltDegree]="value")

configure tilt values
<!--movie-card.component.ts-->
<div class="movie-card" tilt [tiltDegree]="360">
    <!--  content-->
</div>

Great job! Serve the application and test your result with different inputs. For sure test out 360 and other values, have fun ;)

ng serve

Full Solution

TiltDirective
import { Directive, ElementRef, input, OnInit } from '@angular/core';

@Directive({
  selector: '[tilt]',
})
export class TiltDirective implements OnInit {
  tiltDegree = input(5);

  constructor(private element: ElementRef<HTMLElement>) {}

  ngOnInit() {
    this.element.nativeElement.addEventListener('mouseleave', () => {
      this.element.nativeElement.style.transform = 'rotate(0deg)';
    });

    this.element.nativeElement.addEventListener('mouseenter', event => {
      const pos = this.determineDirection(event.pageX);
      this.element.nativeElement.style.transform = `rotate(${pos === 0 ? `${this.tiltDegree()}deg` : `-${this.tiltDegree()}deg`})`;
    });
  }

  /**
   *
   * returns 0 if entered from left, 1 if entered from right
   */
  determineDirection(pos: number): 0 | 1 {
    const width = this.element.nativeElement.clientWidth;
    const middle =
      this.element.nativeElement.getBoundingClientRect().left + width / 2;
    return pos > middle ? 1 : 0;
  }
}
MovieCardComponent
import { Component, input, model } from '@angular/core';

import { MovieModel } from '../../shared/model/movie.model';
import { TiltDirective } from '../../shared/tilt.directive';
import { StarRatingComponent } from '../../ui/pattern/star-rating/star-rating.component';

@Component({
  selector: 'movie-card',
  imports: [StarRatingComponent, TiltDirective],
  template: `
    <div class="movie-card" tilt [tiltDegree]="360">
      <img
        class="movie-image"
        [alt]="movie().title"
        [src]="'https://image.tmdb.org/t/p/w342' + movie().poster_path" />
      <div class="movie-card-content">
        <div class="movie-card-title">{{ movie().title }}</div>
        <div class="movie-card-rating">
          <ui-star-rating [rating]="movie().vote_average" />
        </div>
      </div>
      <button
        class="favorite-indicator"
        [class.is-favorite]="favorite()"
        (click)="toggle()">
        @if (favorite()) {
          I like it
        } @else {
          Like me
        }
      </button>
    </div>
  `,
  styles: `
    .movie-card {
      transition:
        box-shadow 0.15s cubic-bezier(0.4, 0, 0.2, 1) 0s,
        transform 0.25s cubic-bezier(0.4, 0, 0.2, 1) 0s;
    }

    .movie-card:hover {
      .movie-image {
        transform: scale(1);
      }
      box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.6);
    }

    .movie-image {
      display: block;
      width: 100%;
      height: auto;
      transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1) 0s;
      transform: scale(0.97);
    }

    .movie-card-content {
      text-align: center;
      padding: 1.5rem 3rem;
      font-size: 1.5rem;
    }

    .movie-card-title {
      font-size: 2rem;
    }
  `,
})
export class MovieCardComponent {
  movie = input.required<MovieModel>();
  favorite = model(false);

  toggle() {
    this.favorite.set(!this.favorite());
  }
}