Автоматическое скрытие сообщения тоста в Angular

Ранее мы создали сервис для обработки ошибок пользовательского интерфейса путем создания сообщения тоста, сегодня мы улучшим поведение тоста, установим таймаут и автоматическое скрытие.

Настройка таймаута

Таймаут является переменной, но вы не хотите думать об этом, поэтому мы создадим несколько упакованных опций, чтобы определить наиболее известные таймауты. Начнем со свойства для тайм-аута и посмотрим, как с ним работать.

export interface IToast {
  text?: string;
  css?: string;
  extracss?: string;
  buttons?: IToastButton[];
  timeout?: number; // new for timeout to hide
}

@Injectable({ providedIn: 'root' })
export class Toast {
  // ...

  // keep track of timeout
  private isCancled: Subscription;

  // change default to have default 5 seconds delay
  private defaultOptions: IToast = {
    // ...
    timeout: 5000,
  };

  Show(code: string, options?: IToast) {
    // we need to hide before we show in case consecutive show events
    // this will reset the timer
    this.Hide();

    // ...

    // timeout and hide
    this.isCanceled = timer(_options.timeout).subscribe(() => {
      this.Hide();
    });

  }
  Hide() {
    // reset the timer
    // in case of showing two consecutive messages or user clicks dismiss
    if (this.isCanceled) {
      this.isCanceled.unsubscribe();
    }
    this.toast.next(null);
  }
Войти в полноэкранный режим Выход из полноэкранного режима

Идея проста: создать таймер для таймаута и отменить (или сбросить) таймер перед показом или когда пользователь нажимает кнопку «Отмена». Использование простое, но может быть улучшено (таймаут необязателен):

this.toast.ShowSuccess('INVALID_VALUE', {timeout: 1000});

Вместо того чтобы передавать явный тайм-аут, мы хотим иметь варианты времени, в основном три: короткое, долгое и никогда. Мы можем переопределить таймаут как enum:

// toast model
export enum EnumTimeout {
  Short = 4000, // 4 seconds
  Long = 20000, // 20 seconds
  Never = -1, // forever
}

export interface IToast {
  // ... redefine
  timeout?: EnumTimeout; // new for timeout to hide
}

// state service
@Injectable({ providedIn: 'root' })
export class Toast {
  // ...
  // we can set to the default to "short" or any number
  private defaultOptions: IToast = {
   // ...
   timeout: EnumTimeout.Short, // or you can use Config value
  };

  Show(code: string, options?: IToast) {
    // ...
    // if timeout, timeout and hide
    if (_options.timeout > EnumTimeout.Never) {
      this.isCanceled = timer(_options.timeout).subscribe(() => {
        this.Hide();
      });
    }
  }
  //...
}
Войти в полноэкранный режим Выход из полноэкранного режима

Чтобы использовать его, мы можем передать его как число или как enum:

this.toast.Show('SomeCode', {timeout: EnumTimeout.Never});

Теперь перейдем к некоторым бредням о вопросах UX.

Зачем скрывать и как долго

Руководство по материалам для закусочных панелей позволяет отображать одно сообщение поверх предыдущего (в направлении z). Когда пользователь убирает текущее сообщение, старое сообщение под ним остается на месте. Это имеет серьезные недостатки, когда речь идет о пользовательском опыте. Закуски и тосты предназначены для немедленного и контекстного привлечения внимания. Показывать несвежую закуску шумно. Вот почему я выбрал приведенную выше реализацию, которая позволяет показывать одно сообщение за раз, которое отменяется новыми сообщениями.

Мы должны тщательно обдумать, какое сообщение показывать пользователю, когда и как долго. В противном случае, значение тоста, это тост! Общее правило таково: если есть другие визуальные подсказки, сообщение должно быть коротким. Это также означает, что успешные операции редко нуждаются в тостах.

Ниже приведены возможные рецепты, с которыми вы можете согласиться:

Неверные поля формы при отправке

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

Успешные действия без визуальной реакции

Вспомните действие Facebook sharing, созданный пост визуально не обновляет временную шкалу. Идеальным вариантом будет короткое и милое сообщение с тостом и действием для просмотра поста.

Генерируемые системой сообщения с визуальными подсказками

При получении push-уведомления о входящей электронной почте или взаимодействии, когда другой элемент на странице также обновляется, в данном случае значок колокольчика, короткий и действенный тост может быть правильным ответом, отсутствие тоста также может быть другим способом, вспомните уведомления Twitter на рабочем столе.

Генерируемые системой сообщения без визуальных подсказок

Когда PWA-сайт имеет новую версию и хочет предложить пользователю «обновиться», или новому пользователю предлагается «подписаться» на рассылку новостей, длинное нежелательное сообщение с действием звучит правильно. Решающим фактором является то, насколько срочным является сообщение, возможно, это будет «липкое» сообщение.

Такие контексты редко являются шоу-стопперами, и иногда обновление страницы устраняет любые затянувшиеся проблемы, тостовое сообщение существует для того, чтобы прервать внимание, а не для того, чтобы завладеть им. Теперь рассмотрим следующее.

Несвежая страница требует действий

Если страница открыта слишком долго и авторизованный пользователь вышел по таймеру, когда пользователь нажимает на любое действие, требующее авторизации, перенаправьте его на страницу входа и покажите короткий тост с указанием причины.

Неактуальная страница с необязательным действием

Если авторизация необязательна, и пользователь может зарегистрироваться или войти, то тост должен содержать кнопки действий, и не должен исчезать, пока пользователь не отклонит его, или другой тост не отменит его.

Сервер приостанавливает процесс

Когда сервер просто отказывается завершить процесс после длительного времени по неизвестной причине, тост об ошибке должен быть там, чтобы сообщить пользователю, что процесс не был завершен. Возможно, пользователь на некоторое время отошел от экрана (возможно, он думает, что сайт слишком застенчив, чтобы заниматься своими делами, пока он смотрит 😏).

Ошибки API 404

Общие ошибки API 404 также должны задерживаться, потому что нет другого визуального сигнала, чтобы указать на них, но если страница перенаправляется, не нужно показывать никаких сообщений.

Анимация

Последнее, что необходимо добавить, — это анимация. Основные составляющие анимации заключаются в том, чтобы тост сначала появился, появился в поле зрения, остался, скрылся из виду, а затем исчез. Существует несколько способов добиться этого, вот несколько из них:

1 — Анимация элемента без удаления

Первый и самый прямой способ — отказаться от условного существования тоста и просто заставить его нырнуть под нижнюю часть области просмотра. Это делается для того, чтобы избежать необходимости скрывать элемент из DOM после того, как он был удален Angular.

CSS-анимация выглядит следующим образом:

.toast {
  /* ...  remember the bottom: 10px */
  /*by default is should be out of view*/
  /* calculate 100% of layer height plus the margin from bottom */
  transform: translateY(calc(100% + @space));
  transition: transform 0.2s ease-in-out;
}
.toast.inview {
  /*transition back to 0*/
  transform: translateY(0);
}
Вход в полноэкранный режим Выход из полноэкранного режима

В нашем состоянии и модели тостов мы добавляем новое свойство для видимости. Мы инициируем наше состояние со значением по умолчанию false и обновляем это свойство вместо того, чтобы обнулять состояние:

// toast model
export interface IToast {
  // ...
  visible?: boolean;
}

// state
@Injectable({ providedIn: 'root' })
export class Toast {

  // ...
  private defaultOptions: IToast = {
    // ...
    // add default visible false
    visible: false
  };

  // set upon initialization
  constructor() {
    this.toast.next(this.defaultOptions);
  }
  Show(code: string, options?: IToast) {
    // ...
    // update visible to true
    this.toast.next({ ..._options, text: message, visible: true });

    // ... timeout and hide
  }
  Hide() {
    // ...
    // reset with all current values
    this.toast.next({ ...this.toast.getValue(), visible: false });
 }
}
Вход в полноэкранный режим Выход из полноэкранного режима

И, наконец, в шаблоне компонента мы добавляем условный класс inview:

 <ng-container *ngIf="toastState.toast$ | async as toast">
  <div
    [class.inview]="toast.visible"
    class="{{toast.css}} {{toast.extracss}}">
    ...
  </div>
</ng-container>
Войти в полноэкранный режим Выход из полноэкранного режима

2- Программируемое скрытие

Мы также можем анимировать, а затем посмотреть конец анимации (animationeend) перед удалением элемента. Это немного извращенно, но если вы настаиваете на удалении элемента тоста после того, как закончите с ним, это дешевле, чем пакет анимации.

В состоянии тоста, используя то же свойство visible, добавленное выше:

// toast state
@Injectable({ providedIn: 'root' })
export class Toast {
  // ...
  Show(code: string, options?: IToast): void {
    // completely remove when new message comes in
    this.Remove();

    // ...
    this.toast.next({ ..._options, text: message, visible: true });

    // ... timeout and Hide
  }

  // make two distinct functions
  Hide() {

    // this is hide by adding state only and letting component do the rest (animationend)
    this.toast.next({ ...this.toast.getValue(), visible: false });
  }

  Remove() {
    if(this.isCanceled) {
      this.isCanceled.unsubscribe();
    }
    // this removes the element
    this.toast.next(null);
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

В нашем css мы добавляем анимационные последовательности:

.toast {
  /*...*/

  /*add animation immediately*/
  animation: toast-in .2s ease-in-out;
}
/*add outview animation*/
.toast.outview {
  animation: toast-out 0.1s ease-in-out;
  animation-fill-mode: forwards;
}

@keyframes toast-in {
    0% {
        transform: translateY(calc(100% + 10px);
    }
    100% {
        transform: translateY(0);
    }
}

@keyframes toast-out {
    0% {
        transform: translateY(0);
    }

    100% {
        transform: translateY(calc(100% + 10px));
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, в нашем компоненте мы делаем поворот, смотрим animationend, чтобы убрать тост.

@Component({
    selector: 'gr-toast',
    template: `
    <ng-container *ngIf="toastState.toast$ | async as toast">
    <!-- here add outview when toast is invisible, then watch animationend -->
      <div [class.outview]="!toast.visible" (animationend)="doRemove($event)"
      class="{{ toast.css}} {{toast.extracss}}">
        <div class="text">{{toast.text }}</div>
        <div class="buttons" *ngIf="toast.buttons.length">
            <button *ngFor="let button of toast.buttons"
            [class]="button.css"
            (click)="button.click($event)" >{{button.text}}</button>
        </div>

      </div>
    </ng-container>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    styleUrls: ['./toast.less'],
})
export class ToastPartialComponent {
    constructor(public toastState: Toast) {
    }
    // on animation end, remove element
    doRemove(e: AnimationEvent) {
        if (e.animationName === 'toast-out') {
            this.toastState.Remove();
        }
    }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Выглядит уродливо? Выглядит, поэтому, если мы действительно хотим убрать элемент, наш другой вариант — огромный котел, известный как Angular Animation Package.

3 — Пакет анимации Angular

Пакет анимации Angular решает эту проблему волшебным образом.

Я пытался отследить код, но не смог понять механизм, по которому ngIf игнорируется до окончания анимации. А вы можете найти кролика? Дайте мне знать в комментариях.

Сначала отмените то, что мы сделали выше, и добавьте пакет анимации в корень. В css больше не должно быть анимации, а состояние должно просто показываться и скрываться (свойство visible не нужно). Затем в компоненте мы добавляем следующее:

@Component({
  selector: 'gr-toast',
  template: `
  <ng-container *ngIf="toastState.stateItem$ | async as toast">
    <div @toastHideTrigger class="{{ toast.css}} {{toast.extracss}}" >
      The only change is @toastHideTrigger
      ...
  </ng-container>
  `,
  // add animations
  animations: [
    trigger('toastHideTrigger', [
      transition(':enter', [
        // add transform to place it beneath viewport
        style({ transform: 'translateY(calc(100% + 10px))' }),
        animate('0.2s ease-in', style({transform: 'translateY(0)' })),
      ]),
      transition(':leave', [
        animate('0.2s ease-out', style({transform: 'translateY(calc(100% + 10px))'  }))
      ])
    ]),
  ]
})
// ...
Вход в полноэкранный режим Выход из полноэкранного режима

У вас могут быть свои предпочтения, например, использование пакета анимации в angular, я же не вижу никакой дополнительной пользы. Я предпочитаю простой метод — держать его на странице, никогда не удалять.

Небольшое усовершенствование

Вы, вероятно, заметили, что мы скрываем, прежде чем показать, изменение происходит так быстро, что анимация показа нового сообщения не успевает включиться. Чтобы исправить это, мы можем задержать показ на миллисекунды, чтобы убедиться, что анимация включилась. В нашем методе Show:

// Show method, wait milliseconds before you apply
// play a bit with the timer to get the result you desire
timer(100).subscribe(() => {
  // add visible: true if you are using the first or second method
  this.toast.next({ ..._options, text: message  });
});
Войти в полноэкранный режим Выход из полноэкранного режима

Этот эффект наиболее совершенен, когда мы используем второй (закрученный) метод. Потому что только в нем два последовательных сообщения заставляют первое удаляться без анимации, что является идеальным поведением.

Взгляните на результат на StackBlitz.

Управление состоянием на основе RxJS

Если вы следили за развитием событий, то некоторое время назад я представил управление состоянием на основе RxJS в Angular. Этот тост может использовать его следующим образом:

// to replace state with our State Service
// first, extend the StateService of IToast
export class Toast extends StateService<IToast> {

  // then remove the internal observable
  // private toast: BehaviorSubject<IToast | null> = new BehaviorSubject(null);
  // toast$: Observable<IToast | null> = this.toast.asObservable();

  constructor() {
    // call super
    super();
    // set initial state
    this.SetState(this.defaultOptions);
  }

  // ...
  Show(code: string, options?: IToast) {
    // ...
    // use state instead of this
    // this.toast.next({ ..._options, text: message });
    this.SetState({ ..._options, text: message });
  }
  Hide() {
    // ...
    // use state instead
    // this.toast.next(null);
    this.RemoveState();

    // or update state
    this.UpdateState({ visible: false });
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь шаблон должен смотреть toastState.stateItem$, вместо toastState.toast$.

Вот и все, друзья. Вы нашли кролика? Дайте мне знать.

РЕСУРСЫ

  • Проект StackBlitz
  • Псевдонимы ангулярной анимации :enter и :leave
  • Событие завершения анимации HTML

Автоматическое скрытие сообщения о тосте в Angular, Angular, Дизайн, CSS — Sekrab Garage

Ошибки и тосты

garage.sekrab.com

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