Пользовательский и многократно используемый компонент тоста с анимацией Angular, Async Pipe и BehaviorSubject от RxJS

Доступен в моем репозитории на GitHub


Цели

  • Создать компонент тостов с механизмом условной стилизации
  • Определить состояния с сохраненными строками для их подачи
  • Использовать классные анимации из основных пакетов Angular
  • Запуск тоста из любой точки приложения (с помощью любого сервиса или из любого родительского компонента, который его вызывает).

Рабочий процесс выполнения

  1. Стиль компонента — мы создадим стиль самого компонента и при этом вернемся к концепции условного стиля (используя встроенные директивы Angular).
  2. Создание службы тостов — мы создадим фактическую службу, к которой будут обращаться другие службы или компоненты при необходимости вызвать и заполнить тост.
  3. Двусторонняя привязка компонента — мы сделаем настройки компонента, чтобы его условные переменные/свойства были доступны сервису тостов.

Пропустить вперед

Создание и стилизация компонента
Настройка тостов и первоначальное связывание данных
Добавление стилей SCSS
Привязка массива как класса
Добавление анимации Angular в шаблон
Добавление синтаксиса анимации в файл компонента
Создание службы Toast
Доступ к объектам поведения сервиса с помощью Async Pipe
Двустороннее связывание компонента
Использование Async Pipe в шаблоне


Создание и стилизация компонента

Сначала создайте новый компонент в терминале, выполнив команду :

  $ ng g c components/ui/toast
Войти в полноэкранный режим Выйти из полноэкранного режима

Глобальный доступ

Чтобы получить доступ к этому компоненту из любой точки приложения, добавьте его в файл app.component.html. Созданный нами сервис будет гарантировать, что он не появится, пока мы его не вызовем.

<!--in the app.component.html file-->
<app-toast></app-toast> <!--This is the selector for the toast component-->

<router-outlet></router-outlet>
Вход в полноэкранный режим Выход из полноэкранного режима

Быстрый совет для Z-индекса

Мы хотим, чтобы тост появлялся перед каждым элементом в приложении. Но иногда приложение путается. Чтобы исправить это, вы можете просто добавить (и отслеживать) элементы с Z-индексом в файле styles.scss. Примечание: для того чтобы это работало постоянно, не забудьте добавить родительский класс, который вы в итоге создадите для элемента!

// style.scss

app-root {   z-index: 1; }  
app-toast, .toast-class {   z-index: 2; }

Вход в полноэкранный режим Выход из полноэкранного режима

Настройка тостов и первоначальное связывание данных

  • Мы создадим div с директивой *ngIf. Это сделает так, что тост не появится, пока свойство showsToast не станет true. Далее мы добавим это свойство в файл компонента.
  • Строка {{ toastMessage }} привязывает строку сообщения, назначенную свойству toastMessage, которое мы также найдем в файле компонента далее!
<!--toast.component.html-->
<div *ngIf="showsToast" class="toast-class">  
  <div style="max-width: 160px;">    
    {{ toastMessage }}  
  </div> 

  <!--button-->
  <a
    class="close-btn" 
    (click)="showsToast = !showsToast">    
    <small>      
      Dismiss    
    </small>
  </a>
</div>
Вход в полноэкранный режим Выход из полноэкранного режима
// toast.component.ts
import { Component, OnInit } from '@angular/core';

@Component({  
  selector: 'app-toast',  
  templateUrl: './toast.component.html',  
  styleUrls: ['./toast.component.scss']
})

export class ToastComponent implements OnInit {  
  toastMessage = 'This is a toast'; // This is the string the template is already bound to  
  showsToast = true; // This is what toggles the component to show or hide  

  constructor() { }  

  ngOnInit(): void {  }
}

Вход в полноэкранный режим Выход из полноэкранного режима

Добавление стилей SCSS

Условные стили обычно вращаются вокруг добавления или удаления класса (по моему опыту). Так, элемент может быть оформлен определенным образом, а когда ему задается другое условие, добавляются новые стили (которые могут отменить прежние).

Потрясающая функция, о которой, как мне кажется, мало кто знает, — это селекторы заполнителей! Если вы не знаете о них, вы можете прочитать о них здесь.

Не стесняйтесь использовать мой пакет SCSS на npm для этого примера, так как это то, что я делаю для поддержания высокого уровня! Я буду использовать его для этого примера только для цвета.

Запустите $ npm install @riapacheco/yutes (более подробная инструкция находится на странице npm).

Затем добавьте это в SCSS-файл компонента тоста:

// toast.component.scss
@import '~@riapacheco/yutes/combos.scss';

%default-toast { 
  // You indicate a placeholder selected with a preceding '%'
  position: absolute;
  top: 0;
  right: 0rem;  
  margin: 2rem;  
  display: inline-flex;  
  min-width: 260px;    
  min-height: 70px;  
  max-height: 70px;  
  box-shadow: 6px 6px 12px #00000040;  

  flex-flow: row nowrap;  
  align-items: center;  
  justify-content: space-between;  
  border-left: 6px solid black;  
  padding: 1.5rem;  
  border-radius: 4px;  
  font-size: 0.9rem;
}

// Default toast
.toast-class {  
  @extend %default-toast; // You then add the styles to another selector with the @extend decorator
}

// Now we can make our state-specific classes

// Success
.success-toast {  
  @extend %default-toast;  
  border-left: 6px solid $success;
}

// Warning
.warning-toast {  
  @extend %default-toast;  
  border-left: 6px solid $warning;
}

// Danger
.danger-toast {  
  @extend %default-toast;  
  border-left: 6px solid $danger;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Ваш тост по умолчанию выглядит так:

А если вы измените класс элемента с toast-class на warning-toast, то он будет выглядеть следующим образом:


Привязка массива как класса

Теперь, когда вы видите, как меняется компонент с другим классом, мы можем использовать привязку [] для привязки свойства класса к шаблону (которое может изменяться изнутри компонента).

// toast.component.ts
import { Component, OnInit } from '@angular/core';

@Component({  
  selector: 'app-toast',  
  templateUrl: './toast.component.html',  
  styleUrls: ['./toast.component.scss']
})

export class ToastComponent implements OnInit {  
  toastClass = ['toast-class']; // Class lists can be added as an array  
  toastMessage = 'This is a toast';  
  showsToast = true;

  constructor() { }  

  ngOnInit(): void {  }
}
Вход в полноэкранный режим Выход из полноэкранного режима
<!--toast.component.html-->
<div
  *ngIf="showsToast"
  [class]="toastClass"> 
  <!--We bind it by surrounding 'class' with square-brackets and referencing the property from the template-->  
  <div style="max-width: 160px;">    
    {{ toastMessage }}  
  </div>  

  <a
    class="close-btn" 
    (click)="showsToast = !showsToast">    
      <small>      
        Dismiss    
      </small>  
  </a>
</div>

Войти в полноэкранный режим Выход из полноэкранного режима

Добавление анимации Angular в шаблон

Чтобы добавить анимацию с нуля, добавьте модуль BrowserAnimationsModule из пакета angular’s core в файл app.module.ts следующим образом:

@import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({  
  declarations: [    
    AppComponent,    
    ToastComponent  
  ],  
  imports: [    
    BrowserAnimationsModule,
    CommonModule  ],  

  providers: [],  
  bootstrap: [
    AppComponent
  ]
})

export class AppModule { }
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем в файле toast.component.html мы можем добавить триггер анимации, который сообщает приложению, какой элемент мы определим с помощью спецификаций анимации. Поскольку в нем встроен условный триггер, нам больше не нужна директива *ngIf, которую мы добавили ранее. (Круто, да?)

<!--toast.component.html-->
<div
  [@toastTrigger]="showsToast ? 'open' : 'close'" 
  [class]="toastClass">  
  <div style="max-width: 160px;">    
    {{ toastMessage }}  
  </div>  

  <a
    class="close-btn"
    (click)="showsToast = !showsToast">    
    <small>      
      Dismiss    
    </small>  
  </a>
</div>
Вход в полноэкранный режим Выход из полноэкранного режима

Анимации требуется триггер, который вы называете с помощью [@<name>]. Затем он привязывается к условному свойству, которое мы создали ранее, но теперь за ним следует троичный оператор. Таким образом, showsToast ? 'open' : 'close' означает: если свойство showsToast равно true, то используйте open в качестве состояния анимации… иначе используйте close. Теперь мы определим состояния и стили анимации в самом компоненте.

Добавление синтаксиса анимации в файл компонента

Сначала мы должны импортировать в компонент различные элементы анимации, которые мы будем использовать. Подробнее об этих триггерах вы можете узнать из документации angular.

import { Component, OnInit } from '@angular/core';
// import this ⤵
import { animate, state, style, transition, trigger } from '@angular/animations';

@Component({  
  selector: 'app-toast',  
  templateUrl: './toast.component.html',  
  styleUrls: ['./toast.component.scss'],  
  // And then these ⤵
  animations: [    
    trigger('toastTrigger', [ // This refers to the @trigger we created in the template      
      state('open', style({ transform: 'translateY(0%)' })), // This is how the 'open' state is styled      
      state('close', style({ transform: 'translateY(-200%)' })), // This is how the 'close' state is styled      
      transition('open <=> close', [ // This is how they're expected to transition from one to the other         
        animate('300ms ease-in-out')
      ])    
    ])  
  ]
})

export class ToastComponent implements OnInit {  
  toastClass = ['toast-class'];  
  toastMessage = 'This is a toast';  
  showsToast = true;  

  constructor() { }  

  ngOnInit(): void {  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вы можете проверить, работает ли анимация, изменив свойство showsToast на false и добавив функцию setTimeout() для изменения свойства на true через 1000 мс (1 секунду):

export class ToastComponent implements OnInit {  
  toastClass = ['toast-class'];  
  toastMessage = 'This is a toast';  
  showsToast = false;  

  constructor() { }  

  ngOnInit(): void {    
    setTimeout(() => {      
      this.showsToast = true;    
    }, 1000);  
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Запустите $ ng serve в терминале и подождите секунду, пока не появится тост!

Итак, теперь нам нужно, чтобы что-то запускало службу!

3 вещи, которые нужно запомнить

Компонент тоста нуждается в трех входных данных:

  1. Класс (string), который является либо success-toast, warning-toast, либо danger-toast.
  2. Булево значение для showsToast, чтобы либо показать (true), либо скрыть (false), что влияет на анимацию.
  3. Сообщение (string), которое связывается с toastMessage, чтобы сообщение отображалось на экране.

Создание службы Toast

Добавьте службу с помощью следующей команды в терминале

$ ng g s services/toast
Войдите в полноэкранный режим Выйти из полноэкранного режима

(Не забудьте добавить службу в массив providers: [] в ваш файл app.module.ts!

Добавление переменных состояния

В файле toast.service мы сначала добавим constant, который определяет наши состояния тоста как строки. Эти строки будут передаваться компоненту и, таким образом, будут соответствовать классам (например, success-toast), которые мы создали ранее. Это поможет держать все под контролем и облегчит обращение к состояниям из любой точки приложения (без необходимости пересматривать, как они называются, или вводить конкретную строку).

import { Injectable } from '@angular/core';

// Add this constant ⤵
export const TOAST_STATE = {  
  success: 'success-toast',  
  warning: 'warning-toast',  
  danger: 'danger-toast'
};

@Injectable({  
  providedIn: 'root'
})

export class ToastService {  
  constructor() { }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Доступ к поведенческим объектам сервиса с помощью Async Pipe

Супер удобная функция, которая поставляется с Angular, — это асинхронная труба. Вместо того, чтобы вручную подписываться на наблюдаемые объекты внутри компонента, если наблюдаемый объект сервиса/провайдера является публичным, вы можете получить доступ к тому, что находится в наблюдаемом объекте, непосредственно в шаблоне.

Думайте о наблюдаемой как о потоке данных, который вы можете наблюдать и заставлять появляться в разных местах.

Angular часто использует библиотеку RxJS (поставляется с react) для управления состоянием. Поэтому мы будем использовать BehaviorSubjects, поскольку вы можете изначально хранить свойства по умолчанию.

// toast.service.ts

export class ToastService {  
  // The boolean that drives the toast's 'open' vs. 'close' behavior  
  public showsToast$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);  

  // The message string that'll bind and display on the toast  . 
  public toastMessage$: BehaviorSubject<string> = new BehaviorSubject<string>('Default Toast Message');  

  // The state that will add a style class to the component  . 
  public toastState$: BehaviorSubject<string> = new BehaviorSubject<string>(TOAST_STATE.success);   

  constructor() { }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы добавим два метода для запуска компонента и заполнения BehaviorSubjects различными данными.

  1. Метод showToast() будет вызывать тост и передавать через него состояние тоста и строку сообщения тоста (предполагается, что если функция будет вызвана, тост захочет быть открытым).
  2. dismissToast(), чтобы указать, что тост снова стал ложным.
// toast.service.ts

export class ToastService {  
  public showsToast$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);  
  public toastMessage$: BehaviorSubject<string> = new BehaviorSubject<string>('Default Toast Message');  
  public toastState$: BehaviorSubject<string> = new BehaviorSubject<string>(TOAST_STATE.success);  

  constructor() { }  

  showToast(toastState: string, toastMsg: string): void {  
    // Observables use '.next()' to indicate what they want done with observable    
    // This will update the toastState to the toastState passed into the function    
    this.toastState$.next(toastState);    

    // This updates the toastMessage to the toastMsg passed into the function    
    this.toastMessage$.next(toastMsg);    

    // This will update the showsToast trigger to 'true'
    this.showsToast$.next(true);   
  }  

  // This updates the showsToast behavioursubject to 'false'  
  dismissToast(): void {    
    this.showsToast$.next(false);  
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Двустороннее связывание компонента

Теперь мы можем вернуться к компоненту тоста и заменить переменные в шаблоне на наблюдаемые в компоненте.

  1. В самом компоненте, поскольку мы определяем и храним наши значения в сервисе, мы можем удалить значения из свойств компонента и заменить их на их типы.
  2. Мы хотим внедрить сервис toast.service как «public» injectable в конструкторе.
  3. Мы хотим добавить метод dismiss(), который может вызывать сервис toast.service внутри компонента и обращаться к его методу dismissToast().
import { ToastService } from 'src/app/services/toast.service';

export class ToastComponent implements OnInit {  
  // Change the default values to types  
  toastClass: string[];  
  toastMessage: string;  
  showsToast: boolean;  

  constructor(public toast: ToastService ) { } // We inject the toast.service here as 'public'  

  ngOnInit(): void {  }  

  // We'll add this to the dismiss button in the template  
  dismiss(): void {    
    this.toast.dismissToast();  
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Использование асинхронной трубы в шаблоне

Чтобы использовать асинхронный канал, вам нужно просто добавить сервис в качестве объекта, затем имя наблюдаемой, к которой вы обращаетесь, и после этих слов добавить | async.

<!--We canged the toastTrigger and class-->
<div
  [@toastTrigger]="(toast.showsToast$ | async) ? 'open' : 'close'" 
  [class]="toast.toastState$ | async">  
    <div style="max-width: 160px;">
      <!--We access the toastMessage$ observable in the service-->    
      {{ toast.toastMessage$ | async }}  
    </div>  

  <a
    class="close-btn" 
    (click)="dismiss()">    
    <small>      
      Dismiss    
    </small>  
  </a>
</div>
Вход в полноэкранный режим Выход из полноэкранного режима

Проверьте это!

Поскольку объекты поведения могут хранить начальные значения, мы можем протестировать тост, изменив значение по умолчанию showsToast$ на true и добавив к остальным значениям все, что захотите!

export class ToastService {  
  public showsToast$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);  
  public toastMessage$: BehaviorSubject<string> = new BehaviorSubject<string>('This is a test message!');  
  public toastState$: BehaviorSubject<string> = new BehaviorSubject<string>(TOAST_STATE.danger);  

  constructor() { }  

  // .. more code
}
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Это было много — но это также статья, которую я хотел бы прочитать до того, как узнал все это. Мы рассмотрели идею разделения сервисов, анимацию, стилизацию scss и многое другое.

Теперь, когда вы завершаете процесс, требующий запуска тоста, вы можете вызвать его появление (и, возможно, даже добавить setTimeout для его удаления!). Вот пример в стандартной службе аутентификации пользователей firebase:

export class AuthService {  

  constructor(
    private fireAuth: AngularFireAuth,    
    private toast: ToastService  
  ) {}  

  registerUser(email: string, password: string): void {    
    this.fireAuth.createUserWithEmailAndPassword(email, password)      
      .then(res => {        
        this.toast.showToast(          
          TOAST_STATE.success,          
          'You have successfully registered!');        
        this.dismissError();        
        console.log('Registered', res);
      })
      .catch(error => {        
        this.toast.showToast(          
          TOAST_STATE.danger,          
          'Something went wrong, could not register'        
        );        
        this.dismissError();        
        console.log('Something went wrong', error.message);      
      });  
  }  

  private dismissError(): void {    
    setTimeout(() => {      
      this.toast.dismissToast();    
    }, 2000);  
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Вот и все. Вот и все.

Риа

Ps. Проверьте этот код в моем репозитории GitHub

Оцените статью
Procodings.ru
Добавить комментарий