Tinder Carousel
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 theview
is updated whenprofiles
changes.The
view
uses {{expression}} to write out values from the componentprops
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 thedocument
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 andpointerdown
’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">
hasliking
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">
hasnoping
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
andpointerup
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.