DoneJS StealJS jQuery++ FuncUnit DocumentJS
4.3.0
5.0.0 3.13.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • experiment
      • Chat Guide
      • TodoMVC Guide
      • ATM Guide
    • getting started
      • Setting Up CanJS
      • Technology Overview
      • Reading the Docs (API Guide)
      • Experimental ES Module Usage
    • recipes
      • Canvas Clock (Simple)
      • Credit Card Guide (Advanced)
      • Credit Card Guide (Simple)
      • CTA Bus Map (Medium)
      • File Navigator Guide (Advanced)
      • File Navigator Guide (Simple)
      • Playlist Editor (Advanced)
      • Signup and Login (Simple)
      • Text Editor (Medium)
      • Tinder Carousel (Medium)
      • TodoMVC with StealJS
      • Video Player (Simple)
    • topics
      • Debugging
      • Forms
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Using Codemods
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Tinder Carousel (Medium)

  • Edit on GitHub

This guide walks you through building a Tinder-like carousel. Learn how 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.
  • Have custom <button>'s for liking and disliking

The final widget looks like:

JS Bin on jsbin.com

The following sections are broken down the folowing 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.

  • How to verify it works - How to make sure the solution works if it's not obvious.

  • The solution - The solution to the problem.

Setup

START THIS TUTORIAL BY CLONING THE FOLLOWING JS BIN:

Click the JS Bin button. The JS Bin will open in a new window. In that new window, under File, click Clone.

JS Bin on jsbin.com

This JS Bin:

  • Loads CanJS's global build. All of it's packages are available as can.X. For example can-component is available as can.Component.
  • Loads the pepjs polyfill for pointer event support.

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 setup 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-component with a tag that matches the name of your custom element. For example:

We will use <evil-tinder> as our custom tag:

can.Component.extend({
  tag: "evil-tinder"
})

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

can.Component.extend({
  tag: "evil-tinder",
  view: `
    <h2>Evil-Tinder</h2>
  `,
  ViewModel{
  }
});

NOTE: We'll make use of the ViewModel property later.

The solution

Update the JavaScript tab to:

can.Component.extend({
  tag: "evil-tinder",
  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>
  `,
  ViewModel: {
  }
});

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

<body noscroll>
    <evil-tinder></evil-tinder>
</body>

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:

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

If we were to remove items on the ViewModel as follows, the images will update:

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

What you need to know

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

    can.Component.extend({
      tag: "evil-tinder",
      view: `
        {{# each(profiles) }}
            <img src="{{img}}"/>
        {{/ each}}
      `,
      ViewModel: {
        profiles: {
          default(){
            return [{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"}];
          }
        }
      }
    });
    

    The default behavior specifies the default value of the profiles property.

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

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

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

    NOTE Use .get(0) to make sure currentProfile changes when a is removed from the list.

How to verify it works

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

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

The solution

Update the JavaScript tab to:

can.Component.extend({
  tag: "evil-tinder",
  view: `
      <div class="header"></div>

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

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

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

  }
});

Add a like button

The problem

  • When someone clicks the like button, console.log LIKED and 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 ViewModel when a DOM event happens:

    <button on:click="doSomething()"></button>
    
  • Those functions (example: doSomething) are usually methods on the Component’s ViewModel. For example, the following creates a doSomething method on the ViewModel:

    can.Component.extend({
      tag: "some-element",
      view: `<button on:click="doSomething('dance')"></button>`,
      ViewModel: {
        doSomething(cmd) {
          alert("doing "+cmd);
        }
      }
    })
    
  • Use .shift to remove an item from the start of an array:

    this.profiles.shift();
    

The solution

Update the JavaScript tab to:

can.Component.extend({
  tag: "evil-tinder",
  view: `
      <div class="header"></div>

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

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

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

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

  }
});

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:

can.Component.extend({
  tag: "evil-tinder",
  view: `
      <div class="header"></div>

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

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

    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();
    },

  }
});

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 ViewModel property to update as the user creates a drag motion.

  • Define a number property on a ViewModel with:

    ViewModel: {
      ...
      howFarWeHaveMoved: "number"
    }
    
  • As 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 connectedCallback of the ViewModel as follows:

      ViewModel: {
        ...
        connectedCallback(el) {
          var 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 connectedCallback like:

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

    As mobile safari doesn't support pointer events, we have already installed the pep pointer event polyfill.

    The polyfill requires that touch-action="none" be added to elements that should dispatch pointer events like:

    <img touch-action="none"/>
    

    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:

can.Component.extend({
  tag: "evil-tinder",
  view: `
      <div class="header"></div>

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

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

    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();
    },

    connectedCallback(el) {

      var current = el.querySelector(".current");
      var startingX;

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

        startingX = event.clientX;

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

      });
    }
  }
});

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(expression)}} 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 on the ViewModel:
    get liking() {
      return this.howFarWeHaveMoved >= 100;
    },
    

The solution

Update the JavaScript tab to:

can.Component.extend({
  tag: "evil-tinder",
  view: `
      <div class="header"></div>
      <div class='result {{#if(liking)}}liking{{/if}}'></div>
      <div class="images">
        <div class='current' style="left: {{howFarWeHaveMoved}}px">
          <img src="{{currentProfile.img}}"
               draggable="false"
               touch-action="none"/>
        </div>
        <div class='next'>
          <img src="{{nextProfile.img}}"/>
        </div>
      </div>

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

    get currentProfile() {
      return this.profiles.get(0);
    },
    get nextProfile() {
      return this.profiles.get(1);
    },
    get liking() {
      return this.howFarWeHaveMoved >= 100;
    },

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

    connectedCallback(el) {

      var current = el.querySelector(".current");
      var startingX;

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

        startingX = event.clientX;

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

      });
    }
  }
});

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:

can.Component.extend({
  tag: "evil-tinder",
  view: `
      <div class="header"></div>
      <div class='result {{#if(liking)}}liking{{/if}}
                         {{#if(noping)}}noping{{/if}}'></div>
      <div class="images">
        <div class='current' style="left: {{howFarWeHaveMoved}}px">
          <img src="{{currentProfile.img}}"
               draggable="false"
               touch-action="none"/>
        </div>
        <div class='next'>
          <img src="{{nextProfile.img}}"/>
        </div>
      </div>

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

    get currentProfile() {
      return this.profiles.get(0);
    },
    get nextProfile() {
      return this.profiles.get(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();
    },

    connectedCallback(el) {

      var current = el.querySelector(".current");
      var startingX;

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

        startingX = event.clientX;

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

      });
    }
  }
});

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 for the ViewModel with:

    this.stopListening(document);
    

The solution

Update the JavaScript tab to:

can.Component.extend({
  tag: "evil-tinder",
  view: `
      <div class="header"></div>
      <div class='result {{#if(liking)}}liking{{/if}}
                         {{#if(noping)}}noping{{/if}}'></div>
      <div class="images">
        <div class='current' style="left: {{howFarWeHaveMoved}}px">
          <img src="{{currentProfile.img}}"
               draggable="false"
               touch-action="none"/>
        </div>
        <div class='next'>
          <img src="{{nextProfile.img}}"/>
        </div>
      </div>

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

    get currentProfile() {
      return this.profiles.get(0);
    },
    get nextProfile() {
      return this.profiles.get(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();
    },

    connectedCallback(el) {

      var current = el.querySelector(".current");
      var 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);
        });
      });
    }
  }
});

Add an empty profile

The problem

In this section, we will:

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

What you need to know

  • Use default to create a default property value:

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

The solution

Update the JavaScript tab to:

can.Component.extend({
  tag: "evil-tinder",
  view: `
      <div class="header"></div>
      <div class='result {{#if(liking)}}liking{{/if}}
                         {{#if(noping)}}noping{{/if}}'></div>
      <div class="images">
        <div class='current' style="left: {{howFarWeHaveMoved}}px">
          <img src="{{currentProfile.img}}"
               draggable="false"
               touch-action="none"/>
        </div>
        <div class='next'>
          <img src="{{nextProfile.img}}"/>
        </div>
      </div>

      <div class="footer">
        <button class="dissBtn"
                on:click="nope()">Dislike</button>
        <button class="likeBtn"
                on:click="like()">Like</button>
      </div>
  `,
  ViewModel: {
    profiles: {
      default () {
        return [{img: "https://user-images.githubusercontent.com/78602/40454685-5cab196e-5eaf-11e8-87ac-4af6792994ed.jpg", name: "gru"},
          {img: "https://user-images.githubusercontent.com/78602/40454705-6bf4d3d8-5eaf-11e8-9562-2bd178485527.jpg", name: "hannibal"},
          {img: "https://user-images.githubusercontent.com/78602/40454830-e71178dc-5eaf-11e8-80ee-efd64911e35f.png", name: "joker"},
          {img: "https://user-images.githubusercontent.com/78602/40454681-59cffdb8-5eaf-11e8-94ac-4849ab08d90c.jpg", name: "darth"},
          {img: "https://user-images.githubusercontent.com/78602/40454709-6fecc536-5eaf-11e8-9eb5-3da39730adc4.jpg", name: "norman"},
          {img: "https://user-images.githubusercontent.com/78602/40454711-72b19d78-5eaf-11e8-9732-80155ff8bb52.jpg", name: "stapuft"},
          {img: "https://user-images.githubusercontent.com/78602/40454672-566b4984-5eaf-11e8-808d-cb5afd445e89.jpg", name: "dalek"},
          {img: "https://user-images.githubusercontent.com/78602/40454720-7c3d984c-5eaf-11e8-9fa7-f68ddd33e3f0.jpg", name: "wickedwitch"},
          {img: "https://user-images.githubusercontent.com/78602/40454722-802ef694-5eaf-11e8-8964-ca648368720d.jpg", name: "zod"},
          {img: "https://user-images.githubusercontent.com/78602/40454716-76bef438-5eaf-11e8-9d29-5002260e96e1.jpg", name: "venom"}];
      }
    },
    howFarWeHaveMoved: "number",
    emptyProfile: {
      default () {
        return {img: "http://stickwix.com/wp-content/uploads/2016/12/Stop-Sign-NH.jpg"};
      }
    },

    get currentProfile() {
      return this.profiles.get(0) || this.emptyProfile;
    },
    get nextProfile() {
      return this.profiles.get(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();
    },

    connectedCallback(el) {

      var current = el.querySelector(".current");
      var 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);
        });
      });
    }
  }
});

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

On this page

Get help

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