Q:

Add swipe card tinder on ionic 4

I would like to ask about tinder swipeable card with ionic 4. I don't find the link see below: https://github.com/ionic-team/ionic-ion-swipe-cards
'

A:

I think I implemented it and I hope you can leverage this code and provide feedback.
The component template consists of 3 parts:

indicator for user's choices, it gets visibility (opacity) whenever
the user drags their choice into yes/no direction
actual stack of cards
buttons that user can use to make their choice as an alternative to
dragging

The template code + scss code is as follows:
// HTML:
<div class="tinder" [hidden]="!cards.length">

  <div class="tinder--status">

      <div [style.opacity]="crossVisible? '1':'0'">
          <svg width="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
              <svg:path
                  d="M405 136.798L375.202 107 256 226.202 136.798 107 107 136.798 226.202 256 107 375.202 136.798 405 256 285.798 375.202 405 405 375.202 285.798 256z"
                  fill="#CDD6DD" />
          </svg>
      </div>

      <div [style.opacity]="heartVisible? '1':'0'">
          <svg width="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
              <svg:path
                  d="M349.6 64c-36.4 0-70.7 16.7-93.6 43.9C233.1 80.7 198.8 64 162.4 64 97.9 64 48 114.2 48 179.1c0 79.5 70.7 143.3 177.8 241.7L256 448l30.2-27.2C393.3 322.4 464 258.6 464 179.1 464 114.2 414.1 64 349.6 64zm-80.8 329.3l-4.2 3.9-8.6 7.8-8.6-7.8-4.2-3.9c-50.4-46.3-94-86.3-122.7-122-28-34.7-40.4-63.1-40.4-92.2 0-22.9 8.4-43.9 23.7-59.3 15.2-15.4 36-23.8 58.6-23.8 26.1 0 52 12.2 69.1 32.5l24.5 29.1 24.5-29.1c17.1-20.4 43-32.5 69.1-32.5 22.6 0 43.4 8.4 58.7 23.8 15.3 15.4 23.7 36.5 23.7 59.3 0 29-12.5 57.5-40.4 92.2-28.8 35.7-72.3 75.7-122.8 122z"
                  fill="#FFACE4" />
          </svg>
      </div>

  </div>

  <div class="tinder--cards" (pan)="handlePan($event)" (panend)="handlePanEnd($event)">

      <div #tinderCard class="tinder--card" (transitionend)="handleShift()" *ngFor="let card of cards; let i = index"
          [ngStyle]="{ zIndex: cards.length - i, transform: 'scale(' + (20 - i) / 20 + ') translateY(-' + 20 * i + 'px)' }">

          <img #tinderCardImage [src]="card.img" (load)="tinderCardImage.style.opacity = 1">
          <h3>{{ card.title }}</h3>
          <p>{{ card.description }}</p>

      </div>

  </div>

  <div class="tinder--buttons">

    <button (click)="userClickedButton($event, false)">
        <svg width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
            <svg:path
                d="M405 136.798L375.202 107 256 226.202 136.798 107 107 136.798 226.202 256 107 375.202 136.798 405 256 285.798 375.202 405 405 375.202 285.798 256z"
                fill="#CDD6DD" />
        </svg>
    </button>

    <button (click)="userClickedButton($event, true)">
        <svg width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
            <svg:path
                d="M349.6 64c-36.4 0-70.7 16.7-93.6 43.9C233.1 80.7 198.8 64 162.4 64 97.9 64 48 114.2 48 179.1c0 79.5 70.7 143.3 177.8 241.7L256 448l30.2-27.2C393.3 322.4 464 258.6 464 179.1 464 114.2 414.1 64 349.6 64zm-80.8 329.3l-4.2 3.9-8.6 7.8-8.6-7.8-4.2-3.9c-50.4-46.3-94-86.3-122.7-122-28-34.7-40.4-63.1-40.4-92.2 0-22.9 8.4-43.9 23.7-59.3 15.2-15.4 36-23.8 58.6-23.8 26.1 0 52 12.2 69.1 32.5l24.5 29.1 24.5-29.1c17.1-20.4 43-32.5 69.1-32.5 22.6 0 43.4 8.4 58.7 23.8 15.3 15.4 23.7 36.5 23.7 59.3 0 29-12.5 57.5-40.4 92.2-28.8 35.7-72.3 75.7-122.8 122z"
                fill="#FFACE4" />
        </svg>
    </button>

  </div>

</div>

// SCSS:
.tinder {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-color: rgba(0,0,0,0.4);
  position: absolute;
  left: 0;
  top: 0;
}

.tinder--status {
  position: absolute;
  top: 50%;
  margin-top: -30px;
  z-index: 2;
  width: 100%;
  text-align: center;
  pointer-events: none;
}

.tinder--status > div {
  transition: all 0.3s ease-in-out;
}

.tinder--status svg {
  transition: all 0.3s ease-in-out;
  position: absolute;
  width: 100px;
  margin-left: -50px;
}

.tinder--cards {
  text-align: center;
  display: flex;
  flex-direction: column;
  position: fixed;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  overflow: hidden;
}

.tinder--card {
  display: inline-block;
  width: 260px;
  height: 70%;
  background: #FFFFFF;
  padding-bottom: 40px;
  border-radius: 8px;
  overflow: hidden;
  position: absolute;
  will-change: transform;
  transition: all 0.3s ease-in-out;
  cursor: -webkit-grab;
  cursor: -moz-grab;
  cursor: grab;
}

.moving.tinder--card {
  transition: none;
  cursor: -webkit-grabbing;
  cursor: -moz-grabbing;
  cursor: grabbing;
}

.tinder--card img {
  max-width: 100%;
  max-height: 75%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

.tinder--card h3 {
  margin-top: 16px;
  font-size: 24px;
  padding: 0 16px;
  pointer-events: none;
}

.tinder--card p {
  margin-top: 24px;
  font-size: 16px;
  padding: 0 16px;
  pointer-events: none;
}

.tinder--buttons {
  position: absolute;
  flex: 0 0 100px;
  text-align: center;
  bottom: 20px;
  left: 0;
  right: 0;
}

.tinder--buttons button {
  border-radius: 50%;
  line-height: 50px;
  width: 50px;
  height: 50px;
  border: 0;
  background: #FFFFFF;
  display: inline-block;
  padding-top: 10px;
  margin: 0 12px;
}

.tinder--buttons button:focus {
  outline: 0;
}

Some notes:

the template features *ngFor that replicates each card and positions
it in the stack
we leverage hammer.js' pan and pan-end gesture events to deal with dragging
we are listening for transition-end events to actually remove cards from the stack

// TS:
import { Component, Input, ViewChildren, QueryList, ElementRef, EventEmitter, Output, Renderer2 } from '@angular/core';

@Component({
  selector: 'tinder-ui',
  templateUrl: 'tinder-ui.component.html',
  styleUrls: ['tinder-ui.component.scss'],
})
export class TinderUIComponent {

  @Input('cards') cards: Array<{
    img: string,
    title: string,
    description: string
  }>;

  @ViewChildren('tinderCard') tinderCards: QueryList<ElementRef>;
  tinderCardsArray: Array<ElementRef>;

  @Output() choiceMade = new EventEmitter();

  moveOutWidth: number;
  shiftRequired: boolean;
  transitionInProgress: boolean;
  heartVisible: boolean;
  crossVisible: boolean;

  constructor(private renderer: Renderer2) { 
  }

  userClickedButton(event, heart) {
    event.preventDefault();
    if (!this.cards.length) return false;
    if (heart) {
      this.tinderCardsArray[0].nativeElement.style.transform = 'translate(' + this.moveOutWidth + 'px, -100px) rotate(-30deg)';
      this.toggleChoiceIndicator(false,true);
      this.emitChoice(heart, this.cards[0]);
    } else {
      this.tinderCardsArray[0].nativeElement.style.transform = 'translate(-' + this.moveOutWidth + 'px, -100px) rotate(30deg)';
      this.toggleChoiceIndicator(true,false);
      this.emitChoice(heart, this.cards[0]);
    };
    this.shiftRequired = true;
    this.transitionInProgress = true;
  };

  handlePan(event) {

    if (event.deltaX === 0 || (event.center.x === 0 && event.center.y === 0) || !this.cards.length) return;

    if (this.transitionInProgress) {
      this.handleShift();
    }

    this.renderer.addClass(this.tinderCardsArray[0].nativeElement, 'moving');

    if (event.deltaX > 0) { this.toggleChoiceIndicator(false,true) }
    if (event.deltaX < 0) { this.toggleChoiceIndicator(true,false) }

    let xMulti = event.deltaX * 0.03;
    let yMulti = event.deltaY / 80;
    let rotate = xMulti * yMulti;

    this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', 'translate(' + event.deltaX + 'px, ' + event.deltaY + 'px) rotate(' + rotate + 'deg)');

    this.shiftRequired = true;

  };

  handlePanEnd(event) {

    this.toggleChoiceIndicator(false,false);

    if (!this.cards.length) return;

    this.renderer.removeClass(this.tinderCardsArray[0].nativeElement, 'moving');

    let keep = Math.abs(event.deltaX) < 80 || Math.abs(event.velocityX) < 0.5;
    if (keep) {

      this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', '');
      this.shiftRequired = false;

    } else {

      let endX = Math.max(Math.abs(event.velocityX) * this.moveOutWidth, this.moveOutWidth);
      let toX = event.deltaX > 0 ? endX : -endX;
      let endY = Math.abs(event.velocityY) * this.moveOutWidth;
      let toY = event.deltaY > 0 ? endY : -endY;
      let xMulti = event.deltaX * 0.03;
      let yMulti = event.deltaY / 80;
      let rotate = xMulti * yMulti;

      this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', 'translate(' + toX + 'px, ' + (toY + event.deltaY) + 'px) rotate(' + rotate + 'deg)');

      this.shiftRequired = true;

      this.emitChoice(!!(event.deltaX > 0), this.cards[0]);
    }
    this.transitionInProgress = true;
  };

  toggleChoiceIndicator(cross, heart) {
    this.crossVisible = cross;
    this.heartVisible = heart;
  };

  handleShift() {
    this.transitionInProgress = false;
    this.toggleChoiceIndicator(false,false)
    if (this.shiftRequired) {
      this.shiftRequired = false;
      this.cards.shift();
    };
  };

  emitChoice(heart, card) {
    this.choiceMade.emit({
      choice: heart,
      payload: card
    })
  };

  ngAfterViewInit() {
    this.moveOutWidth = document.documentElement.clientWidth * 1.5;
    this.tinderCardsArray = this.tinderCards.toArray();
    this.tinderCards.changes.subscribe(()=>{
      this.tinderCardsArray = this.tinderCards.toArray();
    })
  };

}

Note re ts part:

@Input is used to get the list of cards
we leverage @Output to emit choices by the user
we use @ViewChildren to keep track of the stack of cards
card is only actually removed (array.shift()) when the transition is finished (transition end event)
the component itself is hidden in case it has no cards in the stack, idea is that we show this component from a page component for example by referencing an array that can contain cards

Hopefully, this is a good example of how such component can be implemented
Editor URL: https://stackblitz.com/edit/ionic-4-template-bks4dd
Demo: https://ionic-4-template-bks4dd.stackblitz.io
Article: medium post

