DoneJS StealJS jQuery++ FuncUnit DocumentJS
6.0.1
5.33.2 4.3.0 3.14.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • getting started
      • CRUD Guide
      • Setting Up CanJS
      • Technology Overview
    • topics
      • HTML
      • Routing
      • Service Layer
      • Debugging
      • Forms
      • Testing
      • Logic
      • Server-Side Rendering
    • app guides
      • Chat Guide
      • TodoMVC Guide
      • TodoMVC with StealJS
    • beginner recipes
      • Canvas Clock
      • Credit Card
      • File Navigator
      • Signup and Login
      • Video Player
    • intermediate recipes
      • CTA Bus Map
      • Multiple Modals
      • Text Editor
      • Tinder Carousel
    • advanced recipes
      • Credit Card
      • File Navigator
      • Playlist Editor
      • Search, List, Details
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Migrating to CanJS 5
      • Migrating to CanJS 6
      • Using Codemods
    • other
      • Reading the API Docs
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Tinder Carousel

  • Edit on GitHub

This intermediate guide walks you through building a Tinder-like carousel. Learn how to build apps that use dragging user interactions.

In this guide, you will learn how to create a custom Tinder-like carousel. The custom widget will have:

  • Touch and drag functionality that works on mobile and desktop.
  • Custom <button>’s for liking and disliking

The final widget looks like:

See the Pen CanJS 6 Tinder-Like Carousel by Bitovi (@bitovi) on CodePen.

The following sections are broken down the following parts:

  • The problem - A description of what the section is trying to accomplish.
  • What you need to know - Information about CanJS that is useful for solving the problem.
  • The solution - The solution to the problem.

Setup

START THIS TUTORIAL BY CLICKING THE “EDIT ON CODEPEN” BUTTON IN THE TOP RIGHT CORNER OF THE FOLLOWING EMBED:

See the Pen CanJS 6 Tinder-Like Carousel by Bitovi (@bitovi) on CodePen.

This CodePen loads CanJS (import { StacheElement } from "//unpkg.com/can@6/core.mjs").

The problem

When someone adds <evil-tinder></evil-tinder> to their HTML, we want the following HTML to show up:

<div class="header"></div>

<div class="images">
  <div class="current">
    <img src="https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg"/>
  </div>
  <div class="next">
    <img src="https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg"/>
  </div>
</div>

<div class="footer">
  <button class="dissBtn">Dislike</button>
  <button class="likeBtn">Like</button>
</div>

What you need to know

To set up a basic CanJS application, you define a custom element in JavaScript and use the custom element in your page’s HTML.

To define a custom element, extend can-stache-element and register it with the tag name you want to use in the HTML.

For example, we will use <evil-tinder> as our custom tag:

class EvilTinder extends StacheElement {}
customElements.define("evil-tinder", EvilTinder);

But this doesn’t do anything. Components add their own HTML through their view property like this:

class EvilTinder extends StacheElement {
  static view = `
    <h2>Evil-Tinder</h2>
  `,
  static props = {};
}
customElements.define("evil-tinder", EvilTinder);

NOTE: We’ll make use of the props object later.

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class EvilTinder extends StacheElement {
  static view = `
    <div class="header"></div>

    <div class="images">
      <div class="current">
        <img src="https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg">
      </div>
      <div class="next">
        <img src="https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg">
      </div>
    </div>

    <div class="footer">
      <button class="dissBtn">Dislike</button>
      <button class="likeBtn">Like</button>
    </div>
  `;

  static props = {};
}

customElements.define("evil-tinder", EvilTinder);

Update the <body> element in the HTML tab to:

<evil-tinder></evil-tinder>

Show the current and next profile images

The problem

Instead of hard-coding the current and next image URLs, we want to show the first two items in the following list of profiles:

[
  { name: "gru", img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg" },
  { name: "hannibal", img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg" },
  { name: "joker", img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png" },
  { name: "darth", img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg" },
  { name: "norman", img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg" },
  { name: "stapuft", img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg" },
  { name: "dalek", img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
  { name: "wickedwitch", img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" },
  { name: "zod", img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg" },
  { name: "venom", img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg" }
]

If we were to remove items on the evil-tinder component as follows, the images will update:

document.querySelector("evil-tinder").profiles.shift()

What you need to know

  • A component's view is rendered with its props. For example, we can create a list of profiles and write out an <img> for each one like:

    class EvilTinder extends StacheElement {
      static view = `
        {{# for(profile of this.profiles) }}
          <img src="{{ profile.img }}">
        {{/ for }}
      `;
    
      static props = {
        get default() {
          return new ObservableArray([
            { img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
            { img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" }
          ]);
        }
      };
    }
    customElements.define("evil-tinder", EvilTinder);
    

    The get default() behavior specifies the default value of the profiles property; can-observable-array is used to make sure the view is updated when profiles changes.

    The view uses {{expression}} to write out values from the component props into the DOM.

  • Use a getter to derive a value from another value on the component props, this will allow us to get the next profile image:

    get currentProfile() {
      return this.profiles[0];
    },
    

How to verify it works

Run the following in the Console tab. The background image should move to the foreground.

document.querySelector("evil-tinder").profiles.shift()

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class EvilTinder extends StacheElement {
  static view = `
    <div class="header"></div>

    <div class="images">
      <div class="current">
        <img src="{{ this.currentProfile.img }}">
      </div>
      <div class="next">
        <img src="{{ this.nextProfile.img }}">
      </div>
    </div>

    <div class="footer">
      <button class="dissBtn">Dislike</button>
      <button class="likeBtn">Like</button>
    </div>
  `;

  static props = {
    profiles: {
      get default() {
        return new ObservableArray([
          { name: "gru", img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg" },
          { name: "hannibal", img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg" },
          { name: "joker", img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png" },
          { name: "darth", img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg" },
          { name: "norman", img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg" },
          { name: "stapuft", img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg" },
          { name: "dalek", img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
          { name: "wickedwitch", img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" },
          { name: "zod", img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg" },
          { name: "venom", img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg" }
        ]);
      }
    },

    get currentProfile() {
      return this.profiles[0];
    },

    get nextProfile() {
      return this.profiles[1];
    }
  };
}

customElements.define("evil-tinder", EvilTinder);

Add a like button

The problem

  • When someone clicks the like button, console.log LIKED, remove the first profile image, and show the next one in the list.

What you need to know

  • Use on:event to call a function on the component when a DOM event happens:

    <button on:click="doSomething()"></button>
    
  • Those functions (example: doSomething) are added as methods on the component like:

    class SomeElement extends StacheElement {
      static view = `<button on:click="doSomething('dance')"></button>`;
      static props = { ... };
      doSomething(cmd) {
        alert("doing " + cmd);
      }
    }
    customElements.define("some-element", SomeElement);
    
  • Use .shift to remove an item from the start of an array:

    this.profiles.shift();
    

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class EvilTinder extends StacheElement {
  static view = `
    <div class="header"></div>

    <div class="images">
      <div class="current">
        <img src="{{ this.currentProfile.img }}">
      </div>
      <div class="next">
        <img src="{{ this.nextProfile.img }}">
      </div>
    </div>

    <div class="footer">
      <button class="dissBtn">Dislike</button>
      <button class="likeBtn" 
              on:click="this.like()">Like</button>
    </div>
  `;

  static props = {
    profiles: {
      get default() {
        return new ObservableArray([
          { name: "gru", img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg" },
          { name: "hannibal", img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg" },
          { name: "joker", img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png" },
          { name: "darth", img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg" },
          { name: "norman", img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg" },
          { name: "stapuft", img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg" },
          { name: "dalek", img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
          { name: "wickedwitch", img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" },
          { name: "zod", img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg" },
          { name: "venom", img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg" }
        ]);
      }
    },

    get currentProfile() {
      return this.profiles.get(0);
    },

    get nextProfile() {
      return this.profiles.get(1);
    }
  };

  like() {
    console.log("LIKED");
    this.profiles.shift();
  }
}

customElements.define("evil-tinder", EvilTinder);

Add a nope button

The problem

  • When someone clicks the nope button, console.log NOPED and remove the first profile.

What you need to know

  • You know everything you need to know

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class EvilTinder extends StacheElement {
  static view = `
    <div class="header"></div>

    <div class="images">
      <div class="current">
        <img src="{{ this.currentProfile.img }}">
      </div>
      <div class="next">
        <img src="{{ this.nextProfile.img }}">
      </div>
    </div>

    <div class="footer">
      <button class="dissBtn"
              on:click="this.nope()">Dislike</button>
      <button class="likeBtn" 
              on:click="this.like()">Like</button>
    </div>
  `;

  static props = {
    profiles: {
      get default() {
        return new ObservableArray([
          { name: "gru", img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg" },
          { name: "hannibal", img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg" },
          { name: "joker", img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png" },
          { name: "darth", img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg" },
          { name: "norman", img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg" },
          { name: "stapuft", img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg" },
          { name: "dalek", img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
          { name: "wickedwitch", img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" },
          { name: "zod", img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg" },
          { name: "venom", img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg" }
        ]);
      }
    },

    get currentProfile() {
      return this.profiles.get(0);
    },

    get nextProfile() {
      return this.profiles.get(1);
    }
  };

  like() {
    console.log("LIKED");
    this.profiles.shift();
  }
  
  nope() {
    console.log("NOPED");
    this.profiles.shift();
  }
}

customElements.define("evil-tinder", EvilTinder);

Drag and move the profile to the left and right

The problem

In this section we will:

  • Move the current profile to the left or right as user drags the image to the left or right.
  • Implement drag functionality so it works on a mobile or desktop device.
  • Move the <div class="current"> element

What you need to know

We need to listen to when a user drags and update the <div class="current"> element’s horizontal position to match how far the user has dragged.

  • To update an element’s horizontal position with can-stache you can set the element.style.left property like:
    <div class="current" style="left: {{ howFarWeHaveMoved }}px">
    

The remaining problem is how to get a howFarWeHaveMoved property to update as the user creates a drag motion.

  • Define a number property on the component props with:

    static props = {
      // ...
      howFarWeHaveMoved: Number
    };
    
  • A drag motion needs to be captured just not on the element itself, but on the entire document, we will setup the event binding in the connected hook of the component as follows:

      class SomeElement extends StacheElement {
        ...
        connected() {
          let current = el.querySelector(".current");
        }
      }
    
  • Desktop browsers dispatch mouse events. Mobile browsers dispatch touch events. Most desktop and dispatch Pointer events.

    You can listen to pointer events with listenTo inside connected like:

    this.listenTo(current, "pointerdown", (event) => { /* ... */ })
    

    Drag motions on images in desktop browsers will attempt to drag the image unless this behavior is turned off. It can be turned off with draggable="false" like:

    <img draggable="false">
    
  • Pointer events dispatch with an event object that contains the position of the mouse or finger:

    this.listenTo(current, "pointerdown", (event) => {
      event.clientX //-> 200
    });
    

    On a pointerdown, this will be where the drag motion starts. Listen to pointermove to be notified as the user moves their mouse or finger.

  • Listen to pointermove on the document instead of the dragged item to better tollerate drag motions that extend outside the dragged item.

    this.listenTo(document, "pointermove", (event) => {
    
    });
    

    The difference between pointermove’s position and pointerdown’s position is how far the current profile <div> should be moved.

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class EvilTinder extends StacheElement {
  static view = `
    <div class="header"></div>

    <div class="images">
      <div class="current" style="left: {{ this.howFarWeHaveMoved }}px">
        <img 
          src="{{ this.currentProfile.img }}"
          draggable="false"
        >
      </div>
      <div class="next">
        <img src="{{ this.nextProfile.img }}">
      </div>
    </div>

    <div class="footer">
      <button class="dissBtn"
              on:click="this.nope()">Dislike</button>
      <button class="likeBtn"
              on:click="this.like()">Like</button>
    </div>
  `;

  static props = {
    profiles: {
      get default() {
        return new ObservableArray([
          { name: "gru", img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg" },
          { name: "hannibal", img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg" },
          { name: "joker", img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png" },
          { name: "darth", img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg" },
          { name: "norman", img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg" },
          { name: "stapuft", img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg" },
          { name: "dalek", img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
          { name: "wickedwitch", img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" },
          { name: "zod", img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg" },
          { name: "venom", img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg" }
        ]);
      }
    },

    howFarWeHaveMoved: Number,

    get currentProfile() {
      return this.profiles[0];
    },

    get nextProfile() {
      return this.profiles[1];
    }
  };

  like() {
    console.log("LIKED");
    this.profiles.shift();
  }

  nope() {
    console.log("NOPED");
    this.profiles.shift();
  }

  connected() {
    let current = this.querySelector(".current");
    let startingX;

    this.listenTo(current, "pointerdown", event => {
      startingX = event.clientX;

      this.listenTo(document, "pointermove", event => {
        this.howFarWeHaveMoved = event.clientX - startingX;
      });
    });
  }
}

customElements.define("evil-tinder", EvilTinder);

Show liking animation when you drag to the right

The problem

In this section, we will:

  • Show a like "stamp" when the user has dragged the current profile to the right 100 pixels.
  • The like stamp will appear when an element like <div class="result"> has liking added to its class list.

What you need to know

  • Use if to test if a value is truthy and add a value to an element’s class list like:
    <div class="result {{# if(liking) }}liking{{/ if}}">
    
  • Use a getter to derive a value from another value:
    get liking() {
      return this.howFarWeHaveMoved >= 100;
    },
    

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class EvilTinder extends StacheElement {
  static view = `
    <div class="header"></div>
    <div class="result {{# if(this.liking) }}liking{{/ if }}"></div>
    <div class="images">
      <div class="current" style="left: {{ this.howFarWeHaveMoved }}px">
        <img 
          src="{{ this.currentProfile.img }}"
          draggable="false"
        >
      </div>
      <div class="next">
        <img src="{{ this.nextProfile.img }}"/>
      </div>
    </div>

    <div class="footer">
      <button class="dissBtn"
              on:click="this.nope()">Dislike</button>
      <button class="likeBtn"
              on:click="this.like()">Like</button>
    </div>
  `;

  static props = {
    profiles: {
      get default() {
        return new ObservableArray([
          { name: "gru", img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg" },
          { name: "hannibal", img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg" },
          { name: "joker", img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png" },
          { name: "darth", img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg" },
          { name: "norman", img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg" },
          { name: "stapuft", img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg" },
          { name: "dalek", img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
          { name: "wickedwitch", img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" },
          { name: "zod", img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg" },
          { name: "venom", img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg" }
        ]);
      }
    },

    howFarWeHaveMoved: Number,

    get currentProfile() {
      return this.profiles[0];
    },

    get nextProfile() {
      return this.profiles[1];
    },

    get liking() {
      return this.howFarWeHaveMoved >= 100;
    }
  };

  like() {
    console.log("LIKED");
    this.profiles.shift();
  }

  nope() {
    console.log("NOPED");
    this.profiles.shift();
  }

  connected() {
    let current = this.querySelector(".current");
    let startingX;

    this.listenTo(current, "pointerdown", event => {
      startingX = event.clientX;

      this.listenTo(document, "pointermove", event => {
        this.howFarWeHaveMoved = event.clientX - startingX;
      });
    });
  }
}

customElements.define("evil-tinder", EvilTinder);

Show noping animation when you drag to the left

The problem

  • Show a nope "stamp" when the user has dragged the current profile to the left 100 pixels.
  • The nope stamp will appear when an element like <div class="result"> has noping added to its class list.

What you need to know

You know everything you need to know!

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class EvilTinder extends StacheElement {
  static view = `
    <div class="header"></div>
    <div class="result {{# if(this.liking) }}liking{{/ if }}
                       {{# if(this.noping) }}noping{{/ if }}"></div>
    <div class="images">
      <div class="current" style="left: {{ this.howFarWeHaveMoved }}px">
        <img 
          src="{{ this.currentProfile.img }}"
          draggable="false"
        >
      </div>
      <div class="next">
        <img src="{{ this.nextProfile.img }}"/>
      </div>
    </div>

    <div class="footer">
      <button class="dissBtn"
              on:click="this.nope()">Dislike</button>
      <button class="likeBtn"
              on:click="this.like()">Like</button>
    </div>
  `;

  static props = {
    profiles: {
      get default() {
        return new ObservableArray([
          { name: "gru", img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg" },
          { name: "hannibal", img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg" },
          { name: "joker", img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png" },
          { name: "darth", img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg" },
          { name: "norman", img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg" },
          { name: "stapuft", img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg" },
          { name: "dalek", img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
          { name: "wickedwitch", img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" },
          { name: "zod", img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg" },
          { name: "venom", img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg" }
        ]);
      }
    },

    howFarWeHaveMoved: Number,

    get currentProfile() {
      return this.profiles[0];
    },

    get nextProfile() {
      return this.profiles[1];
    },

    get liking() {
      return this.howFarWeHaveMoved >= 100;
    },
    
    get noping() {
      return this.howFarWeHaveMoved <= -100;
    }
  };

  like() {
    console.log("LIKED");
    this.profiles.shift();
  }

  nope() {
    console.log("NOPED");
    this.profiles.shift();
  }

  connected() {
    let current = this.querySelector(".current");
    let startingX;

    this.listenTo(current, "pointerdown", event => {
      startingX = event.clientX;

      this.listenTo(document, "pointermove", event => {
        this.howFarWeHaveMoved = event.clientX - startingX;
      });
    });
  }
}

customElements.define("evil-tinder", EvilTinder);

On release, like or nope

The problem

In this section, we will perform one of the following when the user completes their drag motion:

  • console.log like and move to the next profile if the drag motion has moved at least 100 pixels to the right
  • console.log nope and move to the next profile if the drag motion has moved at least 100 pixels to the left
  • do nothing if the drag motion did not move 100 pixels horizontally

And, we will perform the following no matter what state the drag motion ends:

  • Reset the state of the application so it can accept further drag motions and the new profile image is centered horizontally.

What you need to know

  • Listen to pointerup to know when the user completes their drag motion:

    this.listenTo(document, "pointerup", (event) => { });
    
  • To stopListening to the pointermove and pointerup events on the document with:

    this.stopListening(document);
    

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class EvilTinder extends StacheElement {
  static view = `
    <div class="header"></div>
    <div class="result {{# if(this.liking) }}liking{{/ if }}
                       {{# if(this.noping) }}noping{{/ if }}"></div>
    <div class="images">
      <div class="current" style="left: {{ this.howFarWeHaveMoved }}px">
        <img 
          src="{{ this.currentProfile.img }}"
          draggable="false"
        >
      </div>
      <div class="next">
        <img src="{{ this.nextProfile.img }}"/>
      </div>
    </div>

    <div class="footer">
      <button class="dissBtn"
              on:click="this.nope()">Dislike</button>
      <button class="likeBtn"
              on:click="this.like()">Like</button>
    </div>
  `;

  static props = {
    profiles: {
      get default() {
        return new ObservableArray([
          { name: "gru", img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg" },
          { name: "hannibal", img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg" },
          { name: "joker", img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png" },
          { name: "darth", img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg" },
          { name: "norman", img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg" },
          { name: "stapuft", img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg" },
          { name: "dalek", img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
          { name: "wickedwitch", img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" },
          { name: "zod", img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg" },
          { name: "venom", img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg" }
        ]);
      }
    },

    howFarWeHaveMoved: Number,

    get currentProfile() {
      return this.profiles[0];
    },

    get nextProfile() {
      return this.profiles[1];
    },

    get liking() {
      return this.howFarWeHaveMoved >= 100;
    },
    
    get noping() {
      return this.howFarWeHaveMoved <= -100;
    }
  };

  like() {
    console.log("LIKED");
    this.profiles.shift();
  }

  nope() {
    console.log("NOPED");
    this.profiles.shift();
  }

  connected() {
    let current = this.querySelector(".current");
    let startingX;

    this.listenTo(current, "pointerdown", event => {
      startingX = event.clientX;

      this.listenTo(document, "pointermove", event => {
        this.howFarWeHaveMoved = event.clientX - startingX;
      });
      
      this.listenTo(document, "pointerup", event => {
        this.howFarWeHaveMoved = event.clientX - startingX;

        if (this.liking) {
          this.like();
        } else if (this.noping) {
          this.nope();
        }

        this.howFarWeHaveMoved = 0;
        this.stopListening(document);
      });
    });
  }
}

customElements.define("evil-tinder", EvilTinder);

Add an empty profile

The problem

In this section, we will:

  • Show the following stop sign URL when the user runs out of profiles: https://stickwix.com/wp-content/uploads/2016/12/Stop-Sign-NH.jpg.

What you need to know

  • Use get default() to create a default property value:

    emptyProfile: {
      get default() {
        return { img: "https://stickwix.com/wp-content/uploads/2016/12/Stop-Sign-NH.jpg" };
      }
    },
    

The solution

Update the JavaScript tab to:

import { StacheElement } from "//unpkg.com/can@6/core.mjs";

class EvilTinder extends StacheElement {
  static view = `
    <div class="header"></div>
    <div class="result {{# if(this.liking) }}liking{{/ if }}
                       {{# if(this.noping) }}noping{{/ if }}"></div>
    <div class="images">
      <div class="current" style="left: {{ this.howFarWeHaveMoved }}px">
        <img 
          src="{{ this.currentProfile.img }}"
          draggable="false"
        >
      </div>
      <div class="next">
        <img src="{{ this.nextProfile.img }}"/>
      </div>
    </div>

    <div class="footer">
      <button class="dissBtn"
              on:click="this.nope()">Dislike</button>
      <button class="likeBtn"
              on:click="this.like()">Like</button>
    </div>
  `;

  static props = {
    profiles: {
      get default() {
        return new ObservableArray([
          { name: "gru", img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg" },
          { name: "hannibal", img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg" },
          { name: "joker", img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png" },
          { name: "darth", img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg" },
          { name: "norman", img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg" },
          { name: "stapuft", img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg" },
          { name: "dalek", img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg" },
          { name: "wickedwitch", img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg" },
          { name: "zod", img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg" },
          { name: "venom", img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg" }
        ]);
      }
    },

    howFarWeHaveMoved: Number,

    emptyProfile: {
      get default() {
        return {
          img: "https://stickwix.com/wp-content/uploads/2016/12/Stop-Sign-NH.jpg"
        };
      }
    },

    get currentProfile() {
      return this.profiles[0] || this.emptyProfile;
    },

    get nextProfile() {
      return this.profiles[1] || this.emptyProfile;
    },

    get liking() {
      return this.howFarWeHaveMoved >= 100;
    },
    
    get noping() {
      return this.howFarWeHaveMoved <= -100;
    }
  };

  like() {
    console.log("LIKED");
    this.profiles.shift();
  }

  nope() {
    console.log("NOPED");
    this.profiles.shift();
  }

  connected() {
    let current = this.querySelector(".current");
    let startingX;

    this.listenTo(current, "pointerdown", event => {
      startingX = event.clientX;

      this.listenTo(document, "pointermove", event => {
        this.howFarWeHaveMoved = event.clientX - startingX;
      });
      
      this.listenTo(document, "pointerup", event => {
        this.howFarWeHaveMoved = event.clientX - startingX;

        if (this.liking) {
          this.like();
        } else if (this.noping) {
          this.nope();
        }

        this.howFarWeHaveMoved = 0;
        this.stopListening(document);
      });
    });
  }
}

customElements.define("evil-tinder", EvilTinder);

Result

When finished, you should see something like the following CodePen:

See the Pen CanJS 6 Tinder-Like Carousel by Bitovi (@bitovi) on CodePen.

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 6.0.1.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news