Создание анимированной кнопки "Поделиться" в NativeScript + Angular

Разработка /
Разработка: main view

Сегодня я покажу вам как создавать анимированную кнопку «Поделиться» в NativeScript и Angular. При нажатии этой кнопки будут показаны маленькие кнопки соцсетей по кругу от главной.

Исходный код примера вы можете увидеть на Github.

Итак, приступим!

Установка


Создадим проект, используя параметр --ng для создания приложения angular:

tns create --ng tns-animated-social-button


В нашем приложении будет использоваться плагин ng2-fonticon от Nathan Walker для вывода иконок на кнопках. Установите его по инструкции на этой странице.
Также мы используем пакет lodash. Установим его:

npm install --save lodash
npm install --save @types/lodash

Теперь приступим к нашему коду.

Создание SocialShareButtonComponent


Ядро нашего приложения будет описано в компоненте SocialShareButtonComponent. В шаблоне будут главная кнопка и несколько кнопок социальных сетей.
При нажатии на главную кнопку выезжают маленькие кнопки, а при повторном нажатии они возвращаются обратно. Для кнопок мы используем иконку «круг» из font awesome. Иконочные шрифты очень хороши тем, что они одинаково выглядят на любом экране и разрешении. При этом нужно помнить, что их размер контролируется параметром font-size. Для того, чтобы сделать необходимый размер компонента, мы должны выполнить некоторые расчёты — это из-за того, что не все иконки в шрифте имеют одинаковый размер.
На вход мы будем принимать массив наименований для иконок. И использовать его для создания соответствующих кнопок. Наименования возьмём из списка иконок font awesome. Теперь, зная всё это, давайте создадим компонент в новой папке social-share-button:

// app/social-share-button/social-share-button.component.html

import { 
  Component,
  Input
} from '@angular/core';
import { TNSFontIconModule } from 'nativescript-ng2-fonticon';

@Component({
  selector: 'social-share-button',
  templateUrl: 'social-share-button/social-share-button.component.html',
  styleUrls: ['social-share-button/social-share-button.component.css']
})
export class SocialShareButtonComponent {
  @Input('size') size = 75;
  @Input('shareIcons') shareIcons: string[];

  public get mainIconSize(): number {
    return this.size * 0.45;
  }

  public get shareButtonSize(): number {
    return this.size * 0.55;
  }

  public get shareIconSize(): number {
    return this.shareButtonSize * 0.5;
  }

  public get viewHeight(): number {
    return this.size + this.shareButtonSize * 1.2;
  }

  public get viewWidth(): number {
    return this.size + this.shareButtonSize * 2.2;
  }


  constructor(private fonticon: TNSFontIconModule) {}
}


В переменной size мы будем хранить расчитанный размер под разные разрешения, установим по-умолчанию его в 75. Она будет отвечать за параметр font-size главной кнопки. Переменная mainIconSize это размер иконки в главной кнопке. Переменная shareButtonSize отвечает за размер других кнопок, а shareIconSize, за размер иконки в них. Свойства viewHeight и viewWidth отвечают за внешние размеры всего представления. Нам нужно достаточно места для отображения главной кнопки, а также всех остальных малых кнопок. У нас будет максимум одна кнопка рядом с главной, поэтому высота никогда не превысит size + shareButtonSize. Что касается ширины, у нас будет по одной кнопке с каждой стороны, а в итоге: size + shareButtonSize x 2. Мы используем коэффициенты в том числе для того, чтобы было немного дополнительного пространства.

Создадим такой шаблон:

<!-- app/component/social-share-button/social-share-button.component.html -->

<GridLayout rows="auto" 
  [style.width]="viewWidth"
  [style.height]="viewHeight">
  <GridLayout #shareButton
    [style.width]="shareButtonSize"
    *ngFor="let shareIcon of shareIcons">
    <Label
      [style.font-size]="shareButtonSize" 
      class="fa button" 
      [text]="'fa-circle' | fonticon">
    </Label>
    <Label [style.font-size]="shareIconSize"
      class="fa share-icon"
      [text]="'fa-' + shareIcon | fonticon"></Label>
  </GridLayout>
  <GridLayout 
    (tap)="onMainButtonTap()"
    [style.width]="size">
    <Label #mainButton
      [style.font-size]="size" 
      class="fa button" 
      [text]="'fa-circle' | fonticon"></Label>
    <Label [style.font-size]="mainIconSize" 
      class="fa share-icon" 
      [text]="'fa-share-alt' | fonticon"></Label>
  </GridLayout>
</GridLayout>

Кнопки помещаются в GridLayout таким образом, чтобы иконки находились поверх кругов. Всё содержимое в свою очередь, помещается в GridLayout, к которому мы динамически применили такие свойства, как высота и ширина.
Для создания кнопок соцсетей мы проходим в цикле по массиву переданных иконок. Текстом иконки будет конкатенация ‘fa-‘ и значения shareIcon.
Затем создадим соответствующую таблицу стилей:

/* app/social-share-button/social-share-button.component.css */

GridLayout {
  text-align: center;
  vertical-align: center;
}

Label.button {
  color: #000;
}

Label.share-icon {
  color: #FFF;
  vertical-align: center;
}


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

Перед тем, как перейти к реализации, выведем результат в AppComponent. Сначала добавим Component в список деклараций AppModule:

// app/app.module.ts

import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/platform";
import { SocialShareButtonComponent } from './social-share-button/social-share-button.component';
import { TNSFontIconModule } from 'nativescript-ng2-fonticon';
import { AppComponent } from "./app.component";


@NgModule({
    declarations: [
      AppComponent,
      SocialShareButtonComponent
    ],
    bootstrap: [AppComponent],
    imports: [
      NativeScriptModule,
      TNSFontIconModule.forRoot({
        'fa': 'font-awesome.css'
      })
    ],
    schemas: [NO_ERRORS_SCHEMA]
})
export class AppModule { }


Затем откроем AppComponent и немного причешем код:

// app/app.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "my-app",
  templateUrl: "app.component.html",
  styleUrls: ['app.component.css']
})
export class AppComponent {
}


Создайте шаблон app.component.html и вставьте в него следующее:

<!-- app/app.component.html -->

<StackLayout class="container">
  <social-share-button 
    [shareIcons]="['facebook', 'twitter', 'github', 'linkedin', 'tumbler']"></social-share-button>
</StackLayout>


И файл CSS:

/* app/app.component.css */

StackLayout.container {
  width: 100%;
  vertical-align: center;
  margin-left: auto;
  margin-right: auto;
} 


В результате должно получиться такое:
Разработка: preview

Анимация кнопок


Сейчас мы поработает над анимациями вокруг главной кнопки. Сперва создадим свойство @ViewChildren() для получения GridLayout-ов всех кнопок:


@ViewChildren('shareButton') shareButtonRefs: QueryList<ElementRef>;

  private get shareButtons(): Array<GridLayout> {
    return this.shareButtonRefs.map(s => s.nativeElement);
  } 


Мы хотим сделать двухэтапные анимации. Сначала кнопки соцсетей вылетают из-за главной кнопки простым линейным перемещением. Затем нам нужно сделать кое-что посложнее — нам нужно, чтобы мелкие кнопки вылетали по кругу от главной. Финальная позиция кнопки в круге будет зависеть от положения других кнопок или, другими словами, от её позиции в массиве shareButtons.
Для создания кругового перемещения вспомним, как расчитываются координаты x, y от точки по краю окружности, в угловой функции:

Разработка: Создание анимированной кнопки Поделиться в NativeScript
где x0 и y0 это координаты начала круга, r это его радиус, а ϴ это угол.

Чтобы мы могли увидеть круговую анимацию, нельзя просто переместить точку по кругу. Это бы привело к пересечению окружности:
Разработка: Создание анимированной кнопки Поделиться в NativeScript
Вместо этого нужно сделать несколько последовательных перемещений, малыми шагами (маленькие вариации ϴ):
Разработка: Создание анимированной кнопки Поделиться в NativeScript
Переведём теперь это в код.

Анимируем малые кнопки вокруг главной


Отследим тап по главной кнопке методом onMainButtonTap() нашего Component:

<!-- app/social-share-button/social-share-button.component.html -->

[...]
  <GridLayout 
    (tap)="onMainButtonTap()"
    [style.width]="size">
[...] 


И соответствующий метод в Component:

// app/social-share-button/social-share-button.component.ts

[...]
import { Animation } from 'ui/animation';
[...]
  constructor(private fonticon: TNSFontIconModule) {}

  public onMainButtonTap(): void {
    const animationDefinitions = this.shareButtons.map(button => {
      return {
        target: button,
        translate: { x: this.size * 0.8, y: 0 },
        duration: 200
      };
    });
    const animation = new Animation(animationDefinitions);
    animation.play();
  }
} 


Перемещение по оси x будет равно значению, прямо пропорциональному размеру главной кнопки. С этого значения и начнутся все вращения. Если вернуться к расчетам координат, то это будет радиусом, вокруг которого мы вращаем кнопки. Зная всё это, создадим свойство-getter для получения этого значения:


[...]
  private get buttonRotationRadius(): number {
    return this.size * 0.8;
  }

[...]
  public onMainButtonTap(): void {
    const animationDefinitions = this.shareButtons.map(button => {
      return {
        target: button,
        translate: { x: this.buttonRotationRadius, y: 0 },
        duration: 200
      };
    });
    const animation = new Animation(animationDefinitions);
    animation.play();
  }
}


Отсюда угол ϴ равен нулю. Теперь перейдём к самому забавному: вращению кнопок.

Круговые перемещения кнопок


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

[...]
import { range } from 'lodash';
[...]

  private maxAngleFor(index: number): number {
    return index * 45;
  }

  private angleIntervals(maxAngle: number): Array<number> {
    const step = 5;
    return range(0, maxAngle + step, step);
  }
}


Метод maxAngleFor() на входе принимает index и возвращает его, умноженным на 45. Это значит, что каждая кнопка будет отделена четвертью круга — для симметрии.
Метод angleIntervals() принимает maxAngle, и возвращает массив последовательных значений с шагом 5, в пределах maxAngle. Это будут наши шаги вращения.
Также мы реализуем метод получения координат точки, соответствующей значению угла:

[...]
import { Animation, Pair } from 'ui/animation';
[...]
  private buttonCoordinatesFor(angle: number): Pair {
    const x = this.buttonRotationRadius * Math.cos(angle * Math.PI / 180);
    const y = this.buttonRotationRadius * Math.sin(angle * Math.PI / 180);

    return { x: x, y: y };
  }
}


Теперь важная задача — сделать перемещения кнопок с привязкой к шагу по окружности. Одно из решений для этого — создать массив из AnimationDefinition, как мы сделали в предыдущем разделе, и вызывать анимации с флагом playSequentially. К сожалению, сделай мы так, это привело бы к очистке представления после каждого шага анимации, что нам абсолютно не нужно. Другое решение — к каждому шагу анимации привязывать возвращённое значение Promise через метод then(). Мы можем сделать это с помощью метода reduce(), вызванного после метода angleIntervals(). Несколько строк кода расскажут нам больше тысячи слов:


[...]
    animation.play().then(() => {
      this.shareButtons.forEach((button, index) => {
        const maxAngle = this.maxAngleFor(index);
          this.angleIntervals(maxAngle).reduce((accumulator, currentAngle, index) => {
            return accumulator.then(() => {
              return button.animate({
                translate: this.buttonCoordinatesFor(currentAngle),
                duration: 0.8
              });
            });
          }, Promise.resolve({}));
      });
[...]


Для каждой кнопки мы получаем соответствующее ей значение maxAngle. И используем его для расчёта угловых шагов, вызывая метод reduce, связывая вместе все Promise (мы начали с результата пустого Promise). Продолжительность анимации занимает всего 0.8 мс, так мы перемещаем кнопку на соответствующие координаты для текущего угла. Напомню, что начинаем мы с угла, равного 0.

После небольшого рефакторинга, это превращается в:


[...]
  public onMainButtonTap(): void {
    this.translateShareButtonsOutOfMainButton().then(() => {
      this.rotateShareButtonsAroundMainButton();
    });
  }

  private translateShareButtonsOutOfMainButton(): AnimationPromise {
    const animationDefinitions = this.shareButtons.map(button => {
      return {
        target: button,
        translate: { x: this.circularRotationRadius, y: 0 },
        duration: 200
      };
    });
    const animation = new Animation(animationDefinitions);
    return animation.play();
  }

  private rotateShareButtonsAroundMainButton(): void {
    this.shareButtons.forEach((button, index) => {
      this.rotateAroundMainButton(button, index);
    });
  }

  private rotateAroundMainButton(button: GridLayout, index: number): AnimationPromise {
    const maxAngle = this.maxAngleFor(index);
    return this.angleIntervals(maxAngle).reduce(
      this.getStepRotationAccumulatorFor(button),Promise.resolve()
    );
  }

  private getStepRotationAccumulatorFor(button: GridLayout) {
    return (accumulator, currentAngle, index) => {
      return accumulator.then(() => this.doStepRotation(button, currentAngle));
    }
  }

  private doStepRotation(button: GridLayout, angle: number): AnimationPromise {
    return button.animate({
      translate: this.buttonCoordinatesFor(angle),
      duration: 0.8
    });
  }
}


Возврат кнопок на место


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


[...]
@Component({
  selector: 'social-share-button',
  templateUrl: 'social-share-button/social-share-button.component.html',
  styleUrls: ['social-share-button/social-share-button.component.css']
})
export class SocialShareButtonComponent {

  private shareButtonDisplayed = false;
[...]


Анимация обратного возврата кнопок будет очень похожа на translateShareButtonsOutOfMainButton(), поэтому мы возьмём содержимое метода, чтобы сделать его более унифицированным:


[...]
  private translateShareButtonsOutOfMainButton(): AnimationPromise {
    return this.translateShareButtonsTo({
      x: this.circularRotationRadius,
      y: 0
    })
  }

  private translateShareButtonsTo(coordinates: Pair): AnimationPromise {
    const animationDefinitions = this.shareButtons.map(button => {
      return {
        target: button,
        translate: coordinates,
        duration: 200
      };
    });
    const animation = new Animation(animationDefinitions);
    return animation.play();
  }
[...]


Что позволит нам написать:


[...]
  private translateShareButtonsBackInMainButton(): AnimationPromise {
    return this.translateShareButtonsTo({ x: 0, y: 0 });
  }
[...]


И теперь мы можем переписать onMainButtonTap():


[...]
  public onMainButtonTap(): void {
    if (!this.shareButtonDisplayed) {
      this.translateShareButtonsOutOfMainButton().then(() => {
        this.rotateShareButtonsAroundMainButton();
      });
    }
    else {
      this.translateShareButtonsBackInMainButton();
    }
    this.shareButtonDisplayed = !this.shareButtonDisplayed;
  }
[...]


Проблема текущей реализации в том, что пользователь может поломать нашу анимацию. Чтобы этого избежать, мы введём переменную-перечисление State, показывающую состояние Component: ожидание, проигрывание или остановлен. Перед этим необходимо переделать метод rotateShareButtonsAroundMainButton() для возврата Promise. В этом методе мы хотим возвращать результат Promise-ов всех анимаций, поэтому мы должны поймать момент окончания всей анимации (остановлен). Изменим метод следующим образом:


[...]
  private rotateShareButtonsAroundMainButton(): AnimationPromise {
    const animationPromises = this.shareButtons.map((button, index) => {
      return this.rotateAroundMainButton(button, index);
    });
    return <AnimationPromise>Promise.all(animationPromises);
  }
[...]


Изменим флаг по состоянию анимации:

[...]
enum AnimationState {
  idle,
  animating,
  settled
}

@Component({
  selector: 'social-share-button',
  templateUrl: 'social-share-button/social-share-button.component.html',
  styleUrls: ['social-share-button/social-share-button.component.css']
})
export class SocialShareButtonComponent {

  private animationState = AnimationState.idle;
[...]


И финальная реализация:


[...]
  public onMainButtonTap(): void {
    if (this.animationState === AnimationState.idle) {
      this.translateShareButtonsOutOfMainButton().then(() => {
        this.animationState = AnimationState.animating;
        return this.rotateShareButtonsAroundMainButton();
      }).then(() => { 
       this.animationState = AnimationState.settled;
      });
    }
    if (this.animationState === AnimationState.settled) {
      this.translateShareButtonsBackInMainButton().then(() => { 
        this.animationState = AnimationState.idle; 
      });
    }
  }
[...]


К этому моменту вы уже должны убедиться в красоте Promise-ов в JavaScript.

Делаем кнопки настраиваемыми


Сейчас наши черно-белые кнопки выглядят очень скучно. Сделаем их настраиваемыми. Добавим пару Input-ов (с некоторыми значениями по-умолчанию):


[...]
  @Input('buttonColor') buttonColor = '#CC0000';
  @Input('iconColor') iconColor = '#FFFFFF';
[...]


И привяжем к шаблону:


<!-- app/social-share-button/social-share-button.component.html -->

<GridLayout rows="auto" 
  [style.width]="viewWidth"
  [style.height]="viewHeight">
  <GridLayout #shareButton
    [style.width]="shareButtonSize"
    *ngFor="let shareIcon of shareIcons">
    <Label
      [style.color]="buttonColor"
      [style.font-size]="shareButtonSize" 
      class="fa button" 
      [text]="'fa-circle' | fonticon">
    </Label>
    <Label [style.font-size]="shareIconSize"
      [style.color]="iconColor"
      class="fa share-icon"
      [text]="'fa-' + shareIcon | fonticon"></Label>
  </GridLayout>
  <GridLayout 
    (tap)="onMainButtonTap()"
    [style.width]="size">
    <Label
      [style.color]="buttonColor"
      [style.font-size]="size" 
      class="fa button" 
      [text]="'fa-circle' | fonticon"></Label>
    <Label [style.font-size]="mainIconSize" 
      [style.color]="iconColor"
      class="fa share-icon" 
      [text]="'fa-share-alt' | fonticon"></Label>
  </GridLayout>
</GridLayout>


А также можно немного подсократить таблицу стилей:


/* app/social-share-button/social-share-button.component.css */

GridLayout {
  text-align: center;
  vertical-align: center;
}

Label.share-icon {
  vertical-align: center;
}


Добавим эффект тени с помощью нативного кода


Немного улучшим стиль кнопки, добавив к ней тень. NativeScript пока не поддерживает показ тени в представлении, поэтому мы сделаем это на нативном коде, с помощью Directive, которая может быть реализована и для iOS и для Android.
Создадим новую папку специально для кода нашей Directive, назовём её label-shadow. Теперь создадим абстрактную базовую директиву, которая будет унаследована каждой платформой:


// app/label-shadow/label-shadow-base.directive.ts

import { Directive, ElementRef } from '@angular/core';
import { Label } from 'ui/label';
import { Observable } from 'data/observable';
import { Color } from 'color';

@Directive({
  selector: '[shadow]'
})

export abstract class LabelShadowBaseDirective {

  private get label(): Label {
    return this.el.nativeElement;
  }

  protected get shadowColor(): Color {
    return new Color('#888888');
  }

  protected get shadowOffset(): number {
    return 5.0;
  }

  constructor(protected el: ElementRef) {
    this.label.on(Observable.propertyChangeEvent, () => {
      if (this.label.text !== undefined) {
        this.displayShadowOn(this.label);
      }
    });
  }

  protected abstract displayShadowOn(label: Label);
}


Нам нужно подождать, пока Label с плагином FontIcon настроится, поэтому добавим хук — перехватчик события. По его готовности мы применим абстрактный метод displayShadowOn().

Перед тем, как взяться за реализацию, создадим описание типов, которое покажет TypeScript, что директива здесь будет во время компиляции:


// app/label-shadow/label-shadow.directive.ts

import { Label } from 'ui/label';
import { LabelShadowBaseDirective } from './label-shadow-base.directive';

export declare class LabelShadowDirective extends LabelShadowBaseDirective {
  constructor(label: Label);
  protected displayShadowOn(label: Label);
}


Создадим реализацию под Android:


// app/label/label-shadow.directive.android.ts

import { Directive, ElementRef } from '@angular/core';
import { Label } from 'ui/label';
import { LabelShadowBaseDirective } from './label-shadow-base.directive';
import { Color } from 'color';

@Directive({
  selector: '[shadow]'
})
export class LabelShadowDirective extends LabelShadowBaseDirective {
  constructor(protected el: ElementRef) {
    super(el);
  }

  protected displayShadowOn(label: Label) {
    const nativeView = label.android;
    nativeView.setShadowLayer(
      10.0,
      this.shadowOffset,
      this.shadowOffset,
      this.shadowColor.android
    );
  }
}


И для iOS:


// app/label-shadow/label-shadow.directive.ios.ts

import { Directive, ElementRef } from '@angular/core';
import { Label } from 'ui/label';
import { Observable } from 'data/observable';
import { LabelShadowBaseDirective } from './label-shadow-base.directive';
import { Color } from 'color';

declare const CGSizeMake: any;

@Directive({
  selector: '[shadow]'
})
export abstract class LabelShadowDirective extends LabelShadowBaseDirective {

  constructor(protected el: ElementRef) {
    super(el);
  }

  protected displayShadowOn(label: Label) {
    const nativeView = label.ios;
    nativeView.layer.shadowColor = this.shadowColor.ios.CGColor;
    nativeView.layer.shadowOffset = CGSizeMake(this.shadowOffset, this.shadowOffset);
    nativeView.layer.shadowOpacity = 1.0;
    nativeView.layer.shadowRadius = 2.0;
  }
}


Затем добавим Directive в декларации AppModule:


// app/app.module.ts

import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
import { NativeScriptModule } from 'nativescript-angular/platform';
import { SocialShareButtonComponent } from './social-share-button/social-share-button.component';
import { TNSFontIconModule } from 'nativescript-ng2-fonticon';
import { AppComponent } from './app.component';
import { LabelShadowDirective } from './label-shadow/label-shadow.directive';

@NgModule({
    declarations: [
      AppComponent,
      SocialShareButtonComponent,
      LabelShadowDirective
    ],
    bootstrap: [AppComponent],
    imports: [
      NativeScriptModule,
      TNSFontIconModule.forRoot({
        'fa': 'font-awesome.css'
      })
    ],
    schemas: [NO_ERRORS_SCHEMA]
})
export class AppModule { }


Теперь мы можем добавить директиву в Label-ы FontIcon, представляющие наши кнопки:


<!-- app/social-share-button/social-share-button.component.html -->

[...]
    <Label
      shadow
      [style.color]="buttonColor"
      [style.font-size]="shareButtonSize" 
      class="fa button" 
      [text]="'fa-circle' | fonticon">
    </Label>
[...]
    <Label
      shadow
      [style.color]="buttonColor"
      [style.font-size]="size" 
      class="fa button" 
      [text]="'fa-circle' | fonticon"></Label>
[...]


Представим Component в разных размерах и цветах. Отредактируем AppComponent:


// app/app.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "my-app",
  templateUrl: "app.component.html",
  styleUrls: ['app.component.css']
})
export class AppComponent {
  public get shareIcons(): Array<string> {
    return ['facebook', 'twitter', 'linkedin', 'github', 'tumblr'];
  }
}


И шаблон:


<!-- app/app.component.html -->

<StackLayout class="container">
  <social-share-button
    [shareIcons]="shareIcons"
    [size]="100">
  <social-share-button
    [buttonColor]="'#581845'"
    [shareIcons]="shareIcons"
    [size]="80">
  <social-share-button
    [buttonColor]="'#FFC300'"
    [iconColor]="'#C70039'"
    [shareIcons]="shareIcons"
    [size]="60">
  <social-share-button
    [buttonColor]="'#99D5FF'"
    [iconColor]="'#000000'"
    [shareIcons]="shareIcons"
    [size]="40">
</StackLayout>


Что даст нам:

Разработка: Создание анимированной кнопки Поделиться в NativeScript

Результат нажатой кнопки «Поделиться»


Кнопки уже выглядят хорошо, но они бесполезны, потому что ничего не делают. Введём EventEmitter Output, который будет показывать имя иконки, когда соответствующая кнопка будет нажата:


// app/social-share-button.component.ts

[...]
  @Output('shareButtonTap') shareButtonTap = new EventEmitter<string>();
[...]


Затем привяжем хук к (tap) GridLayout-а кнопки на метод onShareButton(), передавая ему название иконки:


<!-- app/social-share-button/social-share-button.component.html -->

[...]
  <GridLayout #shareButton
    [style.width]="shareButtonSize"
    *ngFor="let shareIcon of shareIcons"
    (tap)="onShareButtonTap(shareIcon)">
[...]


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

// app/social-share-button/social-share-button.component.ts

[...]
  public onShareButtonTap(icon: string): void {
    this.shareButtonTap.emit(icon);
  }
[...]


Это позволяет подписаться на событие в AppComponent:

<!-- app/app.component.html -->

[...]
  <social-share-button
    [shareIcons]="shareIcons"
    [size]="100"
    (shareButtonTap)="onShareButtonTap($event)"></social-share-button>
[...]


// app/app.component.ts

import { Component } from "@angular/core";
import * as dialogs from 'ui/dialogs';

@Component({
  selector: "my-app",
  templateUrl: "app.component.html",
  styleUrls: ['app.component.css']
})
export class AppComponent {
  public get shareIcons(): Array<string> {
    return ['facebook', 'twitter', 'linkedin', 'github', 'tumblr'];
  }

  public onShareButtonTap(event: string): void {
    dialogs.alert(`share on: ${event}`);
  }
}


Добавим немного проверок


В последнем шаге добавим проверки, для того, чтобы предотвратить некорректное использование Component-а.

Окроем ещё раз SocialShareButton, и сделаем так, чтобы он реализовывал интерфейс OnInit:

// app/social-share-button/social-share-button.component.ts

import { 
[...]
  OnInit
} from '@angular/core';

[...]
export class SocialShareButtonComponent implements OnInit {
[...]


затем реализуем перехват ngOnInit() с проверками:

// app/social-share-button.component.ts

[...]
  public ngOnInit() {
    if (!this.shareIcons || this.shareIcons.length === 0) {
      throw new Error('you need to specify at least 1 icon');
    }
    if (this.shareIcons.length > 5) {
      throw new Error('the list of icons cannot contain more than 5 elements');
    }
  }
[...]


Наш Component теперь готов!

Если вам понравился этот материал, не забудьте поделиться им с коллегами!

Источник

0 комментариев
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.