Search code examples
ionic-frameworkionic4swipeswipe-gesturetinder

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

'


Solution

  • 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