In this exercise we want to build our first Attribute Directive
.
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);
}
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)';
});
}
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 MovieCardComponent
s 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
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.
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`})`;
});
}
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
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());
}
}