Good day,
I am trying to split and Axios request into 2 columns when above 720px and 1 column when under 720px.
I managed to achieve this by using CSS with some height: 200vh, display flex, and so on to force it to split into 2 columns, but this is not the correct approach.
There is a way of doing it with computed propriety but I just can't get it to work.
My full code is below:
<template>
<div class="container">
<button #click="getPosts" class="btn btn-info btn-lg mb-5">
Load List
</button>
<h4 v-if="errorMsg">{{ errorMsg }}</h4>
<!-- <table class="table table-striped table-hover">
<tbody class="list__wrapper">
<tr v-for="post in posts" :key="post.id">
<td>{{ post.title }}</td>
</tr>
</tbody>
</table> -->
<div class="container">
<div class="col" v-for="column in columns" :key="column.id">
<div class="item-container" v-for="post in column" :key="post.id">
{{ post.title }}
</div>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "PostList",
data() {
return {
posts: [],
errorMsg: "",
cols: 2,
};
},
computed: {
columns() {
let columns = [];
let mid = Math.ceil(this.post.length / this.cols);
for (let col = 0; col < this.cols; col++) {
columns.push(this.post.slice(col * mid, col * mid + mid));
}
return columns;
},
},
methods: {
getPosts() {
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((response) => {
console.log(response.data);
this.posts = response.data;
})
.catch((error) => {
console.log(error);
this.errorMsg = "Error retrieving data";
});
},
},
};
</script>
<style scoped>
body {
margin: 0;
padding: 0;
font-family: "Ubuntu", sans-serif;
}
.list {
list-style: none;
}
.list__wrapper {
display: flex;
flex-direction: column;
flex-wrap: wrap;
/* height: 510vh; */
}
.table-hover tbody tr:hover {
background-color: #759c99;
cursor: pointer;
padding: 10px;
transition: 0.3s;
}
.table td {
padding: 5px;
border: none;
}
#media screen and (min-width: 720px) {
tbody {
-moz-column-count: 4;
-moz-column-gap: 20px;
-webkit-column-count: 4;
-webkit-column-gap: 20px;
column-count: 2;
column-gap: 20px;
}
.list__wrapper {
/* height: 150vh; */
}
}
/* test */
.container {
display: flex;
border: 1px solid;
}
.col {
margin: 10px;
border: 1px solid;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.item-container {
border: 1px solid;
padding: 5px;
margin: 5px;
}
</style>
I fixed it eventualy from CSS:
.list__wrapper {
display: block;
column-count: 2;
column-gap: 40px;
column-rule: 1px double #759c99;
}
Related
I am currently working on the Body portion for a Spotify clone using ReactJS. I am fairly new to Javascript, HTML, and CSS so apologies for any oversights.
I am having a problem where I cannot scroll down to the bottom of the playlists page even when there are more tracks to be shown. I have tried adding 'overflow-y: scroll' to my CSS which renders a scroll bar but even then, the page cannot be scrolled (see right scrollbar in screenshot below). I have included my Body.jsx below.
Screenshot of Spotify clone
Body.jsx
import React, { useEffect } from 'react'
import styled from 'styled-components';
import {AiFillClockCircle} from "react-icons/ai";
import axios from 'axios';
import { useStateProvider } from '../utils/StateProvider';
import { reducerCases } from "../utils/Constants";
export default function Body() {
const[ {token, selectedPlaylistId, selectedPlaylist} ,dispatch] = useStateProvider();
useEffect(() => {
const getInitialPlaylist = async () => {
const response = await axios.get(
`https://api.spotify.com/v1/playlists/${selectedPlaylistId}`, {
headers: {
Authorization: "Bearer " + token,
"Content-Type": "application/json",
},
}
);
const selectedPlaylist = {
id: response.data.id,
name: response.data.name,
description: response.data.description.startsWith("<a") ? "" : response.data.description,
image: response.data.images[0].url,
tracks: response.data.tracks.items.map(({track}) => ({
id: track.id,
name: track.name,
artists: track.artists.map((artist) => artist.name),
image: track.album.images[2].url,
duration: track.duration_ms,
album: track.album.name,
context_uri: track.album.uri,
track_number: track.track_number,
})),
};
dispatch({type: reducerCases.SET_PLAYLIST,selectedPlaylist});
};
getInitialPlaylist();
},[token,dispatch, selectedPlaylistId]);
return (
<Container>
{
selectedPlaylist && (
<>
<div className="playlist">
<div className="image">
<img src={selectedPlaylist.image} alt="selectedplaylist" />
</div>
<div className="details">
<span className="type">PLAYLIST</span>
<h1 className="title">{selectedPlaylist.name}</h1>
<p className="description">{selectedPlaylist.description}</p>
</div>
</div>
<div className="list">
<div className="header__row">
<div className="col">
<span>#</span>
</div>
<div className="col">
<span>TITLE</span>
</div>
<div className="col">
<span>ALBUM</span>
</div>
<div className="col">
<span><AiFillClockCircle /></span>
</div>
</div>
<div className="tracks">
{
selectedPlaylist.tracks.map(( {id,name,artists,image,duration,album,context_uri,track_number},index) => {
return (
<div className="row" key={id}>
<div className="col">
<span>{index+1}</span>
</div>
<div className="col detail">
<div className="image">
<img src={image} alt="track" />
</div>
<div className="info">
<span className="name">{name}</span>
<span>{artists}</span>
</div>
</div>
<div className="col">
<span>{album}</span>
</div>
<div className="col">
<span>{duration}</span>
</div>
</div>
);
})
}
</div>
</div>
</>
)
}
</Container>
);
}
//CSS for styled components
const Container = styled.div`
overflow-y: scroll; //scroll bar appears but doesn't scroll
.playlist {
margin: 0 2rem;
display: flex;
align-items: center;
gap: 2rem;
.image {
img {
height: 15rem;
box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px;
}
}
.details {
display: flex;
flex-direction: column;
gap: 1rem;
color: #e0dede;
.title {
color: white;
font-size: 4rem;
}
}
}
.list {
.header__row {
display: grid;
grid-template-columns: 0.3fr 3fr 2fr 0.1fr;
margin: 1rem 0 0 0;
color: #dddcdc;
position: sticky;
top: 15vh;
padding: 1rem 3rem;
transition: 0.3s ease-in-out;
}
.tracks {
margin: 0 2rem;
display: flex;
flex-direction: column;
margin-bottom: 5rem;
.row {
padding: 0.5rem 1rem;
display: grid;
grid-template-columns: 0.3fr 3.1fr 2fr 0.1fr;
&:hover {
background-color: rgba(0, 0, 0, 0.7);
}
.col {
display: flex;
align-items: center;
color: #dddcdc;
img {
height: 40px;
width: 40px;
}
}
.detail {
display: flex;
gap: 1rem;
.info {
display: flex;
flex-direction: column;
}
}
}
}
}
`;
If anyone has any ideas as to how I can get the page to be scrollable, I would greatly appreciate it. I have done lots of research but nothing has worked. Thank you so much!
I was working on the same project and faced the same issue.
I figured out that the issue lied in Spotify.jsx file.
Let this be the code:
.body {
height: 100%;
width: 100%;
overflow: auto;
&::-webkit-scrollbar {
width: 0.7rem;
max-height: 2rem;
&-thumb {
background-color:rgba(255, 255, 255, 0.6);
}
}
}
I have an automatic sliding carousel which is working fine except that when it reach the last image then it just freeze on the last image instead of auto slide to the first image. I just can't remake my javascript code alone. Strange but autosliding to the first image was working a few months ago. I had nothing change to the code but seems after last updates of chrome its just not working correctly neither on pc neither on mobile devices. Here is my javascript code:
const carousels = document.querySelectorAll('.img-carousel');
const prevBtn = document.querySelectorAll('.prev');
const nextBtn = document.querySelectorAll('.next');
let carsouselImages = document.querySelectorAll('.img-carousel div');
//Next Carousel
carousels.forEach((carousel, index)=>{
const nextCarousel = () => {
if(carsouselImages[carsouselImages.length - 1]) {
carousel.scrollTo(0, 0);
}
carousel.scrollBy(300, 0);
};
nextBtn[index].addEventListener('click', e => {
nextCarousel();
});
//Prev Carousel
const prevCarousel = () => {
if(carsouselImages[0]) {
carousel.scrollTo(4800,0);
}
carousel.scrollBy(-300, 0);
};
prevBtn[index].addEventListener('click', e => {
prevCarousel();
});
// Auto carousel
const auto = true; // Auto scroll
const intervalTime = 5000;
let sliderInterval;
if (auto) {
sliderInterval = setInterval(nextCarousel, intervalTime);
};
carousel.addEventListener('mouseover', (stopInterval) => {
clearInterval(sliderInterval);
});
carousel.addEventListener('mouseleave', (startInterval) => {
if (auto) {
sliderInterval = setInterval(nextCarousel, intervalTime);
}
});
//for mobile events
carousel.addEventListener('touchstart', (stopIntervalT) => {
clearInterval(sliderInterval);
});
carousel.addEventListener('touchend', (startIntervalT) => {
if (auto) {
sliderInterval = setInterval(nextCarousel, intervalTime);
}
});
//Debounce
var previousCall;
window.addEventListener('resize', () => {
if (previousCall >= 0) {
clearTimeout(previousCall);
}
});
});
Here are css and html codes if needed:
/** img-carousel **/
#imgages-carousel {
display: grid;
align-items: center;
justify-items: center;
padding: 40px 0px;
}
#imgages-carousel1 {
display: grid;
align-items: center;
justify-items: center;
padding: 40px 0px;
}
.img-carousel-container {
width: 800px;
position: relative;
}
.img-carousel {
display: flex;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
padding-bottom: 5px;
}
.img-carousel div {
flex: none;
scroll-snap-align: start;
width: 800px;
position: relative;
}
.img-carousel div img {
display: block;
width: 100%;
object-fit: cover;
}
.img-carousel div p {
position: absolute;
top: 0;
right: 10px;
background: rgba(0,0,0,0.5);
color: #fff;
padding: 5px;
border-radius: 10px;
}
.img-carousel-container button {
position: absolute;
top: calc(50% - 15px);
transform: translateY(-50%);
border: none;
background-color: rgba(255, 193, 7, 0.7);
color: #000;
cursor: pointer;
padding: 10px 15px;
border-radius: 50%;
outline: none;
opacity: 0;
transition: all ease-in-out 0.5s;
}
.prev {
left: 10px;
}
.next {
right: 10px;
}
.img-carousel-container:hover button {
opacity: 1;
}
.img-carousel-container button:hover {
background-color: #ffc107;
}
/** custom scrollbar **/
.img-carousel::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.img-carousel::-webkit-scrollbar-thumb {
background: #ffc107;
border-radius: 10px;
}
.img-carousel::-webkit-scrollbar-track {
background: transparent;
}
.img-carousel-container:hover .img-carousel::-webkit-scrollbar-thumb {
visibility: visible;
}
#media screen and (max-width: 800px) {
.img-carousel-container {
width: 100%;
}
.img-carousel div {
width: 100%;
}
}
html:
<!-- section images carousel -->
<section id="imgages-carousel">
<div class="img-carousel-container">
<div class="img-carousel">
<div>
<img src="https://source.unsplash.com/9Nok_iZEgLk/800x450">
<p>1/6</p>
</div>
<div>
<img src="https://source.unsplash.com/4v7ubW7jz1Q/800x450">
<p>2/6</p>
</div>
<div>
<img src="https://source.unsplash.com/rtCujH697DU/800x450">
<p>3/6</p>
</div>
<div>
<img src="https://source.unsplash.com/ELv8fvulR0g/800x450">
<p>4/6</p>
</div>
<div>
<img src="https://source.unsplash.com/LoPGu6By90k/800x450">
<p>5/6</p>
</div>
<div>
<img src="https://source.unsplash.com/Ndz3w6MCeWc/800x450">
<p>6/6</p>
</div>
</div>
<button class="prev"><i class="fas fa-chevron-left fa-2x"></i></button>
<button class="next"><i class="fas fa-chevron-right fa-2x"></i></button>
</div>
</section>
<section id="imgages-carousel1">
<div class="img-carousel-container">
<div class="img-carousel">
<div>
<img src="https://source.unsplash.com/9Nok_iZEgLk/800x450">
<p>1/6</p>
</div>
<div>
<img src="https://source.unsplash.com/4v7ubW7jz1Q/800x450">
<p>2/6</p>
</div>
<div>
<img src="https://source.unsplash.com/rtCujH697DU/800x450">
<p>3/6</p>
</div>
<div>
<img src="https://source.unsplash.com/ELv8fvulR0g/800x450">
<p>4/6</p>
</div>
<div>
<img src="https://source.unsplash.com/LoPGu6By90k/800x450">
<p>5/6</p>
</div>
<div>
<img src="https://source.unsplash.com/Ndz3w6MCeWc/800x450">
<p>6/6</p>
</div>
</div>
<button class="prev"><i class="fas fa-chevron-left fa-2x "></i></button>
<button class="next"><i class="fas fa-chevron-right fa-2x "></i></button>
</div>
</section>
This is the JSX of the component in question:
import React from 'react'
import * as classes from './PhotoCard.module.scss'
const PhotoCard = ({ img, name, username, profileImg, style }) =>
<div className={classes.PhotoCardWrapper} style={style}>
<div className={classes.ImgWrapper}>
{img}
</div>
<div className={classes.Sidebar}>
<div className={classes.Creator}>
<div className={classes.ProfilePicture}>
{profileImg}
</div>
<div className={classes.ProfileInfo}>
<h5>{name}</h5>
<p>{username}</p>
</div>
</div>
<div className={classes.Comments}>
<input type="text" name="comment" id="comment" placeholder="Write a comment" />
</div>
</div>
</div>
export default PhotoCard
Here is the relevant SCSS:
.PhotoCardWrapper {
width: 100%;
display: flex;
justify-content: flex-start;
align-items: flex-start;
box-sizing: border-box;
border-radius: 5px;
box-shadow: 0 2px 6px rgba(0,0,0,0.16), 0 2px 6px rgba(0,0,0,0.2);
.ImgWrapper {
display: flex;
width: 100%;
img {
width: 100%;
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
}
.Sidebar {
width: 300px;
height: 100%;
display: flex;
flex-flow: column nowrap;
.Creator {
width: 100%;
display: flex;
padding: 10px;
overflow: hidden;
overflow-x: auto;
box-sizing: border-box;
.ProfilePicture {
width: 40px;
height: 40px;
border-radius: 40px;
background-image: url('../../static/defaults/placeholder.png');
background-size: contain;
cursor: pointer;
margin-right: 10px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
border-radius: 40px;
}
}
.ProfileInfo {
height: 40px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
}
h5 {
font-weight: 500;
letter-spacing: 0.5px;
}
p {
font-size: 0.7em;
color: $color-2;
}
}
.Comments {
width: 100%;
display: flex;
flex-flow: column nowrap;
flex-grow: 1;
background: #525252;
input {
border: none;
border-top: 1px solid $color-1;
font-size: 0.9em;
padding: 5px;
}
}
}
}
To explain, I'm trying to preserve the aspect ratio of the image on the left side of the component. I can use an absolute unit for the width however I cannot use an absolute height unit as the height of the component will be based upon the aspect ratio of the image.
The "Sidebar" using flexbox will therefore have to match the height of whatever the unspecified height of the image ends up being. I cannot figure out a solution for this. As you can see I'm trying to use flex-grow on the Comments div which will not work without specifying height in parent elements.
This is the solution I found to this:
import React, { useEffect, useState } from 'react'
import * as classes from './_PhotoCard.module.scss'
import PropTypes from 'prop-types'
const PhotoCard = ({ ident, img, name, profileImg }) => {
const [height, setHeight] = useState(null)
const [imgClicked, setImgClicked] = useState(null)
useEffect(() => setHeight(document.getElementsByClassName('_PhotoCard_ImgWrapper__3S6GQ').item(ident).clientHeight), [ident])
const creatorJSX = (
<>
<div className={classes.ProfilePicture}>
{profileImg}
</div>
<div className={classes.ProfileInfo}>
<h5>{name}</h5>
</div>
</>
)
const imgClickedHandler = () => {
if (imgClicked === null) {
setImgClicked(classes.imgClicked)
document.body.style.overflow = "hidden"
} else {
setImgClicked(null)
document.body.style = "none"
}
}
return (
<div className={`${classes.PhotoCardWrapper} ${imgClicked}`} style={window.matchMedia("(min-width: 600px)").matches ? { marginBottom: 40 } : { marginBottom: 20 }}>
<div className={classes.CreatorMobile}>
{creatorJSX}
</div>
<div className={classes.ImgWrapper} onClick={() => imgClickedHandler()}>
{img}
</div>
<div className={classes.Sidebar} style={window.matchMedia("(min-width: 600px)").matches ? { height: height } : null}>
<div className={classes.SidebarTop}>
<div className={classes.Creator}>
{creatorJSX}
</div>
<div className={classes.Comments}/>
</div>
<input type="text" name="comment" id="comment" placeholder="Write a comment" />
</div>
</div>
)
}
PhotoCard.propTypes = {
ident: PropTypes.number,
img: PropTypes.element,
name: PropTypes.string,
profileImg: PropTypes.element,
}
export default PhotoCard
This could only be achieved by implementing some JS witchcraft.
I'm quite new in vue and frontend generally. I've created component to show and like/dislike pictures. I planned on using it both on main app view and in modal which shows user liked ones. I use exactly the same code in both cases, but nothing shows on the modal. I can see that the Set with pictures is passed from main app to modal, but the pictures are not visible. Here is my code. It's probably some silly mistake, but i have nobody to ask about it.
Shibe.vue (picture component)
<template>
<div >
<div>
<img :src="shibe">
</div>
<div v-bind:class="{ liked : isFavoured }">
<button type="button" class="btn btn-danger"
#click="addToFavourites(shibe, $emit('liked', shibe))"
>
<font-awesome-icon icon="thumbs-up"/>
</button>
<button type="button" class="btn btn-dark"
#click="removeFromFavourites(shibe, $emit('unliked', shibe))"
style="float: right">
<font-awesome-icon icon="thumbs-down"/>
</button>
</div>
</div>
</template>
<script>
export default {
name: "shibe",
props: {
favoured: {default: false},
shibe: '',
},
data: function () {
return {
isFavoured: false,
modal: false,
}
},
mounted() {
this.isFavoured = this.favoured;
},
methods: {
addToFavourites() {
this.isFavoured = true;
},
removeFromFavourites() {
this.isFavoured = false;
}
},
}
</script>
<style lang="scss">
/* Optional Styles */
.header {
position: fixed;
top: 10px;
right: 10px;
width: 10%;
height: 20px;
text-align: right;
}
.liked {
background: firebrick;
}
</style>
App.vue
<template>
<div id="app">
<nav class="navbar navbar-light bg-light">
<span class="navbar-brand mb-0 h1">
<button type="button" class="btn btn-primary" #click="showModal()">
Favourites
</button>
</span>
</nav>
<div class="doge" v-for="shibe in shibes">
<shibe :shibe=shibe #liked="addToFavourited" #unliked="removeFromFavourited"></shibe>
</div>
<favs :favourites=favouriteShibes
v-show="isModalVisible"
#close="closeModal"></favs>
</div>
</template>
<script>
import axios from 'axios';
import 'bootstrap/dist/css/bootstrap.css';
import Favs from "./Favs";
import Shibe from "./Shibe";
export default {
name: 'app',
components: {Favs, Shibe},
data() {
return {
shibes: new Set(),
favouriteShibes: new Set(),
isModalVisible: false,
}
},
methods: {
getInitialShibes() {
axios.get(`http://shibe.online/api/shibes?count=12`)
.then(response => {
this.shibes = response.data;
});
},
addToFavourited(shibe) {
this.favouriteShibes.add(shibe);
console.log(this.favouriteShibes);
},
removeFromFavourited(shibe) {
this.favouriteShibes.delete(shibe);
console.log(this.favouriteShibes);
},
scroll() {
window.onscroll = () => {
let bottomOfWindow = document.documentElement.scrollTop + window.innerHeight === document.documentElement.offsetHeight;
if (bottomOfWindow) {
for (let i = 0; i < 4; i++) {
axios.get(`http://shibe.online/api/shibes?count=1`)
.then(response => {
this.shibes.push(response.data);
});
}
}
}
},
showModal() {
this.isModalVisible = true;
},
closeModal() {
this.isModalVisible = false;
}
},
mounted() {
this.scroll();
},
beforeMount() {
this.getInitialShibes();
}
}
</script>
<style lang="scss">
.header {
position: fixed;
top: 10px;
right: 10px;
width: 10%;
height: 20px;
text-align: right;
}
.doge {
border-radius: 2px;
width: 25%;
height: 20%;
margin: 0 auto 15px auto;
padding: 15px;
display: inline-block;
flex-wrap: wrap;
img {
width: 100%;
height: 100%;
border-radius: 2px;
}
}
.doge-div {
width: 25%;
align-items: center;
}
</style>
Favs.vue (Modal component)
<template>
<div class="modal-backdrop">
<div class="modal">
<header class="modal-header">
<slot name="header">
<button #click="console()">Favourite shibes
</button>
<button
type="button"
class="btn-close"
#click="close"
>
x
</button>
</slot>
</header>
<section class="modal-body">
<slot>
<div class="doge" v-for="shibe in favourites">
<shibe :shibe=shibe></shibe>
</div>
</slot>
</section>
</div>
</div>
</template>
<script>
import Shibe from "./Shibe";
export default {
name: "favs",
components: {Shibe},
props: {
favourites: {},
},
methods: {
close() {
this.$emit('close');
},
console() {
console.log(this.favourites);
}
},
}
</script>
<style lang="scss">
.modal-backdrop {
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background: #FFFFFF;
box-shadow: 2px 2px 20px 1px;
overflow-x: auto;
display: flex;
flex-direction: column;
width: 80%;
height: 60%;
position: absolute;
top: 30%;
left: 50%;
transform: translate(-50%, -30%);
}
.modal-header,
.modal-footer {
padding: 15px;
display: flex;
}
.modal-header {
border-bottom: 1px solid #eeeeee;
color: #4AAE9B;
justify-content: space-between;
}
.modal-body {
position: relative;
padding: 20px 10px;
}
.btn-close {
border: none;
font-size: 20px;
padding: 20px;
cursor: pointer;
font-weight: bold;
color: #4AAE9B;
background: transparent;
}
</style>
I’ve been playing around with transitions a bit in Vue. I’ve got a test app that gets from a db then shows in a table format with v-for to populate the table cells. transition-group though, doesn’t seem to work at all. I’ve got:
<table class="table" v-if="showTable">
<thead>
<tr>
<th>#</th>
<th>First Name</th>
<th>Last Name</th>
<th>Username</th>
</tr>
</thead>
<transition-group tag="tbody" enter-active-class="animated fadeInDownBig" leave-active-class="animated fadeOutRight">
<tr v-for="(value, key, index) in detailsData" v-bind:key="key">
<th scope="row">{{value.details_key}}</th>
<td>{{value.first_name}}</td>
<td>{{value.last_name}}</td>
<td><button class="btn btn-danger" #click="deleteEntry(value.details_key, key, $event)">Delete</button></td>
</tr>
</transition-group>
</table>
The classes I’m trying to use are part of Animate.css, they work fine with just tags. I’ve also tried adding a “name” tag and using my own css classes but nothing seems to work.
For first sight, IMO its not working because you are trying to animate table rows - <tr> tags. And this is not possible. Possible solution is to use the CSS display property to simulate <tr> tags but with another tag - <div> for example, but with CSS like this: div { display: table-row } Look for this post, where I show the animated table example, and how I created table without <table>, or any other table related tags.
Vue.component('data-grid', {
template: '#data-grid',
props: ['url', 'columns'],
data () {
return {
users: [],
query: '',
prevKey: 'id',
orderDesc: false
}
},
computed: {
filteredUsers () {
return _.filter(this.users, user =>
_.find(user, prop =>
new RegExp(this.query, 'i').test(prop)
)
)
}
},
methods: {
sortUsers (evt) {
var key = evt.target.dataset.key
if (this.prevKey === key) {
this.users.reverse()
this.orderDesc = !this.orderDesc
} else {
this.users = _.sortBy(this.users, key)
this.orderDesc = false
this.prevKey = key
}
},
updateQuery: _.debounce(function (evt) {
this.query = evt.target.value
}, 300),
clearQuery () {
this.query = ''
},
onCreate (elm) {
elm.style.opacity = 0
},
async onData (elm) {
this.users = await axios
.get(this.url)
.then(res => res.data)
Velocity(elm, "fadeIn", {duration: 600})
}
}
})
new Vue({
el: '#app'
})
.data-grid {
width: 98%;
margin: 10px auto;
padding: 2px;
background-color: white;
border: 2px solid #3F51B5;
overflow: hidden;
}
.table {
display: table;
width: 100%;
font-size: 13px;
font-family: Arial, sans-serif;
color: #263238;
cursor: pointer;
}
.thead {
display: table-header-group;
}
.tbody {
display: table-row-group;
}
.tr {
display: table-row;
}
.td {
display: table-cell;
position: relative;
}
.tr .td:not(:last-child) {
border-right: 2px solid white;
}
.thead .td {
padding: 5px 14px;
background-color: #3F51B5;
color: white;
font-weight: bold;
text-align: center;
}
.tbody .td {
padding: 4px;
color: #263238;
text-align: center;
}
.tbody .tr:hover .td {
background-color: #C5CAE9;
}
.tbody .tr:hover .td:not(:last-child) {
border-right: 2px solid #C5CAE9;
}
.tools {
margin: 10px;
box-sizing: border-box;
}
.tools:after {
content: "";
display: block;
clear: both;
}
.search {
float: right;
}
.arrow {
display: inline-block;
position: absolute;
width: 0;
height: 0;
margin-left: 5px;
margin-top: 5px;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
transition: all .6s;
}
.asc {
border-bottom: 6px solid white;
}
.desc {
border-top: 6px solid white;
}
.users-move {
transition: transform .6s;
}
.users-enter-active, .users-leave-active {
transition: all .6s;
}
.users-enter, .users-leave-to {
opacity: 0;
transform: translateY(30px);
}
<div id="app">
<data-grid
url="https://api.mockaroo.com/api/b5f38710?count=8&key=cdbbbcd0"
:columns="{id: 'ID', nick: 'Nick name', first: 'First name', last: 'Last name'}"
></data-grid>
</div>
<template id="data-grid">
<transition
appear
v-on:before-appear="onCreate"
v-on:appear="onData"
>
<div class="data-grid">
<div class="tools">
<div class="search">
<input
type="text"
#input="updateQuery"
:value="query"
placehorder="search..."
>
<button class="clear" #click="clearQuery">clear</button>
</div>
</div>
<div class="table">
<div class="thead" #click="sortUsers">
<div class="tr">
<span v-for="(col, key) in columns" class="td" :data-key=key>
{{ col }}
<span
v-if="prevKey === key"
:class="['arrow', orderDesc ? 'desc' : 'asc']">
</span>
</span>
</div>
</div>
<transition-group name="users" tag="div" class="tbody">
<div class="tr" v-for="row in filteredUsers" :key="row.id">
<span class="td" v-for="column in row">{{ column }}</span>
</div>
</transition-group>
</div>
</div>
</transition>
</template>
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.5.0/velocity.min.js"></script>
<script src="https://unpkg.com/underscore#1.8.3/underscore-min.js"></script>
<script src="https://unpkg.com/vue#2.4.4/dist/vue.min.js"></script>
<script src="https://unpkg.com/axios#0.16.2/dist/axios.min.js"></script>