Tinder Carousel (Medium)
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:
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, underFile
, clickClone
.
This JS Bin:
- Loads CanJS's global build. All of it's packages are available as
can.X
. For example can-component is available ascan.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 theViewModel
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 surecurrentProfile
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 adoSomething
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 theViewModel
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 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:
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'>
hasliking
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'>
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:
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
andpointerup
events on the document for theViewModel
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);
});
});
}
}
});