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>
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>
I'm making a small page for the product based on this tutorial. On the page I have a product component, where I keep a "add to cart" button. The card itself, however, is separated from the component and located inside index.html, that's why my cart property is being kept inside vue app (where app is an id of my root div in index.html).
Problem: I need my "add to cart" button to increment the number in the cart itself. I can't really understand how can I do this using addToCart and updateCart methods, as shown in the tutorial.
Can anyone help me out with this issue? I will appreciate any help! Thanks in advance!
Vue.component('product', {
props: {
premium: {
type: Boolean,
required: true
}
},
template: `
<div id="product">
<div class="product-image">
<img :src="image" />
</div>
<div class="product-info">
<h1>{{ title }}</h1>
<p>Shipping: {{ shipping }}</p>
<p v-if="inStock">In Stock</p>
<p v-else>Out of Stock</p>
<h2>Details</h2>
<ul>
<li v-for="detail in details">{{ detail }}</li>
</ul>
<h3>Colors:</h3>
<div v-for="(variant,index) in variants" :key="variant.variantId">
<div class="color-box" :style="{ backgroundColor: variant.variantColor }" #mouseover="updateProduct(index)"></div>
</div>
<button :class="{ disabledButton: !inStock }" v-on:click="add-to-cart" :disabled="!inStock">Add to Cart</button>
</div>
</div>
`,
data() {
return {
product: "Socks",
brand: "Vue Mastery",
selectedVariant: 0,
details: ["80% cotton", "20% polyester", "Gender-neutral"],
variants: [
{
variantId: 2234,
variantQuantity: 15,
variantColor: "green",
variantImage: "./assets/vmSocks-green.jpg"
},
{
variantId: 2235,
variantQuantity: 0,
variantColor: "blue",
variantImage: "./assets/vmSocks-blue.jpg"
}
]
}
},
methods: {
addToCart() {
this.$emit('add-to-cart')
},
updateProduct(index) {
this.selectedVariant = index
}
},
computed: {
title() {
return this.brand + ' ' + this.product
},
image() {
return this.variants[this.selectedVariant].variantImage
},
inStock() {
if (this.variants[this.selectedVariant].variantQuantity > 0) {
return true
} else {
return false
}
},
shipping() {
if (this.premium) {
return "Free"
} else {
return 2.99
}
}
}
})
var app = new Vue({
el: '#app',
data: {
premium: true,
cart: 0
},
methods: {
updateCart() {
this.cart += 1
}
}
})
body {
font-family: tahoma;
color:#282828;
margin: 0px;
}
.nav-bar {
background: linear-gradient(-90deg, #84CF6A, #16C0B0);
height: 60px;
margin-bottom: 15px;
}
.product {
display: flex;
flex-flow: wrap;
padding: 1rem;
}
img {
border: 1px solid #d8d8d8;
width: 70%;
margin: 40px;
box-shadow: 0px .5px 1px #d8d8d8;
}
.product-image {
width: 80%;
}
.product-image,
.product-info {
margin-top: 10px;
width: 50%;
}
.color-box {
width: 40px;
height: 40px;
margin-top: 5px;
}
.cart {
margin-right: 25px;
float: right;
border: 1px solid #d8d8d8;
padding: 5px 20px;
}
button {
margin-top: 30px;
border: none;
background-color: #1E95EA;
color: white;
height: 40px;
width: 100px;
font-size: 14px;
}
.disabledButton {
background-color: #d8d8d8;
}
.review-form {
width: 400px;
padding: 20px;
margin: 40px;
border: 1px solid #d8d8d8;
}
input {
width: 100%;
height: 25px;
margin-bottom: 20px;
}
textarea {
width: 100%;
height: 60px;
}
.tab {
margin-left: 20px;
cursor: pointer;
}
.activeTab {
color: #16C0B0;
text-decoration: underline;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewpoint" content="width=devide-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Vue app</title>
</head>
<body>
<div class="nav-bar"></div>
<div id="app">
<div class="cart">
<p>Cart({{ cart }})</p>
</div>
<product :premium="premium" #add-to-cart="updateCart"></product>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="prior.js"></script>
</body>
</html>
Change This:
<button :class="{ disabledButton: !inStock }" v-on:click="add-to-cart" :disabled="!inStock">Add to Cart</button>
To be this:
<button :class="{ disabledButton: !inStock }" v-on:click="addToCart" :disabled="!inStock">Add to Cart</button>
So addToCart: and this function will emit add-to-cart event
Also you can immediately run emmit onClick without adding function:
<button :class="{ disabledButton: !inStock }" v-on:click="$emit('add-to-cart')" :disabled="!inStock">Add to Cart</button>
Vue.component('product', {
props: {
premium: {
type: Boolean,
required: true
}
},
template: `
<div id="product">
<div class="product-image">
<img :src="image" />
</div>
<div class="product-info">
<h1>{{ title }}</h1>
<p>Shipping: {{ shipping }}</p>
<p v-if="inStock">In Stock</p>
<p v-else>Out of Stock</p>
<h2>Details</h2>
<ul>
<li v-for="detail in details">{{ detail }}</li>
</ul>
<h3>Colors:</h3>
<div v-for="(variant,index) in variants" :key="variant.variantId">
<div class="color-box" :style="{ backgroundColor: variant.variantColor }" #mouseover="updateProduct(index)"></div>
</div>
<button :class="{ disabledButton: !inStock }" v-on:click="addToCart" :disabled="!inStock">Add to Cart</button>
</div>
</div>
`,
data() {
return {
product: "Socks",
brand: "Vue Mastery",
selectedVariant: 0,
details: ["80% cotton", "20% polyester", "Gender-neutral"],
variants: [
{
variantId: 2234,
variantQuantity: 15,
variantColor: "green",
variantImage: "./assets/vmSocks-green.jpg"
},
{
variantId: 2235,
variantQuantity: 0,
variantColor: "blue",
variantImage: "./assets/vmSocks-blue.jpg"
}
]
}
},
methods: {
addToCart() {
this.$emit('add-to-cart')
},
updateProduct(index) {
this.selectedVariant = index
}
},
computed: {
title() {
return this.brand + ' ' + this.product
},
image() {
return this.variants[this.selectedVariant].variantImage
},
inStock() {
if (this.variants[this.selectedVariant].variantQuantity > 0) {
return true
} else {
return false
}
},
shipping() {
if (this.premium) {
return "Free"
} else {
return 2.99
}
}
}
})
var app = new Vue({
el: '#app',
data: {
premium: true,
cart: 0
},
methods: {
updateCart() {
this.cart += 1
}
}
})
body {
font-family: tahoma;
color:#282828;
margin: 0px;
}
.nav-bar {
background: linear-gradient(-90deg, #84CF6A, #16C0B0);
height: 60px;
margin-bottom: 15px;
}
.product {
display: flex;
flex-flow: wrap;
padding: 1rem;
}
img {
border: 1px solid #d8d8d8;
width: 70%;
margin: 40px;
box-shadow: 0px .5px 1px #d8d8d8;
}
.product-image {
width: 80%;
}
.product-image,
.product-info {
margin-top: 10px;
width: 50%;
}
.color-box {
width: 40px;
height: 40px;
margin-top: 5px;
}
.cart {
margin-right: 25px;
float: right;
border: 1px solid #d8d8d8;
padding: 5px 20px;
}
button {
margin-top: 30px;
border: none;
background-color: #1E95EA;
color: white;
height: 40px;
width: 100px;
font-size: 14px;
}
.disabledButton {
background-color: #d8d8d8;
}
.review-form {
width: 400px;
padding: 20px;
margin: 40px;
border: 1px solid #d8d8d8;
}
input {
width: 100%;
height: 25px;
margin-bottom: 20px;
}
textarea {
width: 100%;
height: 60px;
}
.tab {
margin-left: 20px;
cursor: pointer;
}
.activeTab {
color: #16C0B0;
text-decoration: underline;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewpoint" content="width=devide-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Vue app</title>
</head>
<body>
<div class="nav-bar"></div>
<div id="app">
<div class="cart">
<p>Cart({{ cart }})</p>
</div>
<product :premium="premium" #add-to-cart="updateCart"></product>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="prior.js"></script>
</body>
</html>
In my methods I have:
Vue.component('app', {
data: function () {
return {
messages: '',
state: 0,
id: 0,
name: "",
content: "",
todo: [],
columns: ["todo", "doing", "done"],
showModal: false,
popupActivo: false,
}
},
methods:
callMethod: function (event) {
this.todo.push({
messages: this.messages,
state: this.state,
id: this.id++,
})
},
callPopup: function (item) {
item.messages = this.messages,
this.todo.push({
content: this.content,
})
console.log(item.messages, item.content)
},
},
And my template:
<form v-on:submit.prevent="callMethod">
<label for="new-todo">add</label>
<input v-model="messages" id="new-todo">
<button>Send</button>
</form>
<ul>
<li v-if="item.state === 0" v-for="(item, key) in todo"
v-bind:messages="todo.messages + todo.state + todo.id + todo.name + todo.content" :key="item.id"
v-bind:id="key">
<span>{{item.messages}}</span></br>
<span>{{item.content}}</span></br>
<a href="#popup1">
<button>Edit</button>
</a>
<div id="popup1" class="overlay">
<div class="popup">
<a class="close" href="#">×</a>
<form v-on:submit.prevent="callPopup(item)">
<label for="new-todo">Add name</label>
<input v-model="messages" id="new-todo"> </br>
<label for="new-todo">Add content</label>
<input v-model="content" id="new-todo"></br>
<button>Send</button>
</form>
</div>
</div>
</li>
</ul>
And my css if you want for my popup:
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
transition: opacity 500ms;
visibility: hidden;
opacity: 0;
}
.overlay:target {
visibility: visible;
opacity: 1;
}
.popup {
margin: 70px auto;
padding: 20px;
background: #fff;
border-radius: 5px;
width: 30%;
position: relative;
transition: all 5s ease-in-out;
}
.popup .close {
position: absolute;
top: 20px;
right: 30px;
transition: all 200ms;
font-size: 30px;
font-weight: bold;
text-decoration: none;
color: #333;
}
.popup .close:hover {
color: #06D85F;
}
.popup .content {
max-height: 30%;
overflow: auto;
}
When I open my popup, I went to put my message and add a content. But he modifies all messages in the < li >.
And for content, he doesn't show me anything. I tried with console.log(item.content) in callPopup, he show me "undefined".
With a few assumptions, I recreated your scenario.
<template>
<div class="hello">
<form v-on:submit.prevent="callMethod">
<label for="new-todo"></label>
<input v-model="messages" id="new-todo">
<button>Send</button>
</form>
<ul v-if="state === 0">
<li v-for="(item, key) in todo" :key="item.id">
<p>MSG: {{item.messages}}</p>
<p v-if="item.content">CONTENT: {{item.content}}</p>
<button #click="editToDo = key">
<button>Edit</button>
</button>
</li>
</ul>
<div v-if="editToDo !== null" class="overlay">
<div class="popup">
<button class="close" #click="editToDo=null">×</button>
<form v-on:submit.prevent="callPopup(editToDo)">
<label for="new-todo">Add name</label>
<div>
<p>Message:</p>
<input v-model="messages" class="new-todo">
</div>
<div>
<p>Content:</p>
<input v-model="content" class="new-todo">
</div>
<button>Send</button>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data() {
return {
editToDo: null,
messages: "",
state: 0,
id: 0,
name: "",
content: "",
todo: [],
columns: ["todo", "doing", "done"],
showModal: false,
popupActivo: false
};
},
methods: {
callMethod(event) {
this.todo.push({
messages: this.messages,
state: this.state,
id: this.id++,
content: null
});
},
callPopup(editToDo) {
this.todo[editToDo].messages = this.messages;
this.todo[editToDo].content = this.content;
//reset values
this.messages = "";
this.content = "";
}
}
};
</script>
<style scoped>
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
transition: opacity 500ms;
/* visibility: hidden;
opacity: 0; */
}
li {
display: flex;
justify-content: space-around;
margin: 20px 0;
}
.overlay:target {
visibility: visible;
opacity: 1;
}
.popup {
margin: 70px auto;
padding: 20px;
background: #fff;
border-radius: 5px;
width: 30%;
position: relative;
transition: all 5s ease-in-out;
}
.popup .close {
position: absolute;
top: 20px;
right: 30px;
transition: all 200ms;
font-size: 30px;
font-weight: bold;
text-decoration: none;
color: #333;
}
.popup .close:hover {
color: #06D85F;
}
.popup .content {
max-height: 30%;
overflow: auto;
}
</style>
Working Example
You can see that I used editToDo data property in order to edit an exact item in a list. It is simply an index that I am passing to the method.
I also moved the popup outside of the v-for loop and show it conditionally based on the edited item.
You can also see that initially each item in the list is created with content: null. That helps with updating the item, whenever the edit is performed. I used v-if to only show content if it has been added.
There is still some cleaning up left to be done, removing redundant data properties, etc. but I am going to leave that to you and your preference.
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.