Доступен в моем репозитории на GitHub
- Цели
- Рабочий процесс выполнения
- Пропустить вперед
- Создание и стилизация компонента
- Глобальный доступ
- Быстрый совет для Z-индекса
- Настройка тостов и первоначальное связывание данных
- Добавление стилей SCSS
- Привязка массива как класса
- Добавление анимации Angular в шаблон
- Добавление синтаксиса анимации в файл компонента
- 3 вещи, которые нужно запомнить
- Создание службы Toast
- Добавление переменных состояния
- Доступ к поведенческим объектам сервиса с помощью Async Pipe
- Двустороннее связывание компонента
- Использование асинхронной трубы в шаблоне
- Проверьте это!
- Заключение
Цели
- Создать компонент тостов с механизмом условной стилизации
- Определить состояния с сохраненными
строками
для их подачи - Использовать классные анимации из основных пакетов Angular
- Запуск тоста из любой точки приложения (с помощью любого сервиса или из любого родительского компонента, который его вызывает).
Рабочий процесс выполнения
- Стиль компонента — мы создадим стиль самого компонента и при этом вернемся к концепции условного стиля (используя встроенные директивы Angular).
- Создание службы тостов — мы создадим фактическую службу, к которой будут обращаться другие службы или компоненты при необходимости вызвать и заполнить тост.
- Двусторонняя привязка компонента — мы сделаем настройки компонента, чтобы его условные переменные/свойства были доступны сервису тостов.
Пропустить вперед
Создание и стилизация компонента
Настройка тостов и первоначальное связывание данных
Добавление стилей 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 вещи, которые нужно запомнить
Компонент тоста нуждается в трех входных данных:
- Класс (
string
), который является либоsuccess-toast
,warning-toast
, либоdanger-toast
. - Булево значение для
showsToast
, чтобы либо показать (true
), либо скрыть (false
), что влияет на анимацию. - Сообщение (
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 различными данными.
- Метод
showToast()
будет вызывать тост и передавать через него состояние тоста и строку сообщения тоста (предполагается, что если функция будет вызвана, тост захочет быть открытым). 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);
}
}
Двустороннее связывание компонента
Теперь мы можем вернуться к компоненту тоста и заменить переменные в шаблоне на наблюдаемые в компоненте.
- В самом компоненте, поскольку мы определяем и храним наши значения в сервисе, мы можем удалить значения из свойств компонента и заменить их на их типы.
- Мы хотим внедрить сервис toast.service как «public» injectable в конструкторе.
- Мы хотим добавить метод
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