Realizując frontend dla aplikacji schroniska dla zwierząt, którą tworzę w ramach serii artykułów “Przepisz swój kod na nowo!”, odkryłem ciekawy problem. Chodzi o kwestię animacji, które można dodawać w Angularze poprzez adnotację @Component. Ten szczegół zabrał mi sporo czasu podczas tworzenia funkcjonalności, ale tak to już jest w programowaniu 😉. Zabierzmy się zatem do przedstawienia problemu oraz w jaki sposób się on objawiał.

Kod, który nabroił…

Moim celem było zrobienie modalnego okienka ze szczegółami o danym zwierzaku, które pojawia się po jego wybraniu. Przy odpytaniu backendu uruchamiany jest loader, który zaraz po uzyskaniu odpowiedzi powinien zniknąć. Wtedy frontend posiadając niezbędne dane powinien otworzyć odpowiednie okienko używając animacji. Docelowo miało to wyglądać w następujący sposób.

Działanie animacji dla okienka modalnego
Tak docelowo działa modalne okienko ze szczegółami zwierzaka

Jednak przed osiągnięciem takiego efektu działy się dziwne rzeczy podczas jego zamykania. Niby kod był identyczny, ale jedna, mała różnica powodowała, że nie było żadnej animacji podczas zamykania okienka szczegółów.

Brak animacji podczas zamykania okienka modalnego
To chyba nie tak powinno działać…

Nasza aplikacja w znaczny sposób straciła na płynności, ale z jakiego powodu? Po wielu próbach z zastosowaniem rozwiązań znalezionych na Google udało się! Aplikacja zaczęła działać w sposób zamierzony, ale stało za przyczyną tego błędu?

Przedstawienie problemu w kodzie

Zacznijmy od punktu wyjścia, w którym miałem przygotowany cały kod do tej funkcjonalności. Na początku w głównym module aplikacji należy zaimportować moduł BrowserAnimationsModule, dzięki któremu możemy w ogóle rozmawiać o animacjach.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
...

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ...,
    BrowserAnimationsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Następnie w interesującym nas komponencie należy zdefiniować zmienną animations w adnotacji @Component. Polecam zapoznać się z artykułem, który opisuje w jaki sposób możemy rozpocząć przygodę z animacjami w Angularze. W przypadku mojego kodu wygląda to następująco.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...

@Component({
  ...
  animations: [
    trigger('dialog', [
      transition(':enter', [
        style({ opacity: '0' }),
        animate(300)
      ]),
      transition(':leave', [
        animate(300, style({ opacity: '0' }))
      ])
    ])
  ]
})
export class AnimalDetailsComponent {
  ...
}

Ostatnim krokiem jest przedstawienie elementów HTML, na których ma się odbywać animacja. Dodam jeszcze, że moim celem było zachowanie przezroczystego tła i sprawienie, że tylko okienko modalne pojawia się i znika.

1
2
3
4
5
6
7
<div class="modal modal-display modal-background" *ngIf="animal">
  <div class="modal-dialog modal-dialog-centered" [@dialog]>
    <div class="modal-content">
      ...
    </div>
  </div>
</div>

No i właśnie tutaj jest pies pogrzebany… Trzeba się zastanowić co robi dokładnie robi *ngIf oraz [@dialog].

Rozwiązanie problemu braku animacji wyjścia

Z pomocą przyszedł mi wpis Jared Youtsey, który poprzez czytanie krok po kroku otworzył mi oczy na rozwiązanie problemu. *ngIf sprawia, że jeśli podany w nim warunek nie jest spełniony to dany element znika z DOM, natomiast w przeciwnym razie pojawia się on na witrynie. Atrybut [@dialog] (jego nazwa może być dowolna, oby zgadzała się z tą zadeklarowaną w komponencie) używamy natomiast, aby oznaczyć element, który ma być animowany.

I od razu widać, że przy pojawieniu się naszego okienka modalnego (spełnienie warunku *ngIf) nasza animacja ma prawo się wykonać, ponieważ dany element znajduje się w DOM. Natomiast w chwili zamknięcia okienka znika ono w mało przyjazny sposób. Animacja w tym momencie nie ma obiektu, który mogła by animować co daje nam efekt taki jak powyżej. Rozwiązanie jest naprawdę bardzo proste, wystarczy przenieść [@dialog] do elementu, który posiada warunek *ngIf.

1
2
3
4
5
6
7
<div class="modal modal-display modal-background" *ngIf="animal" [@dialog]>
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      ...
    </div>
  </div>
</div>

Podsumowanie

W chwili pisania tego artykułu ten problem wydaje mi się wręcz trywialny, ale na pierwszy rzut oka nie było go widać. Gdy człowiek po raz pierwszy korzysta z danej funkcjonalności to po prostu nie jest z nią obeznany i ciężko mu zwrócić uwagę na takie detale, które jak się okazuje mają duży wpływ na działanie aplikacji. Po chwili szukania na Stack Overflow znalazłem ten sam problem, który przydarzył się innemu deweloperowi. Moim zdaniem naprawdę trudno jest uniknąć takich błędów, ale to dzięki nim szybciej się uczymy. Ciężko nam będzie zapomnieć o takiej pomyłce i przez to (prawie) na pewno nie popełnimy jej ponownie!