Vue component recycling causing watch to fire unexpectedly - javascript

I'm creating a reusable checkbox component for vue and I've run into an interesting issue caused by vue's recycling of unused components.
This is best seen with an example:
Vue.component("checkbox", {
template: `
<div>
<slot></slot>:
<input type="checkbox" v-model="checked" v-on:change="updateVModel"/>
<span>changed!</span>
</div>
`,
props: {
value: {
type: Boolean,
required: true
}
},
data(){
return {checked: this.value};
},
watch: {
value(val){
this.changeAnimation();
this.checked = val;
}
},
methods: {
changeAnimation(){
let $span = this.$el.querySelector("span");
$span.classList.remove("animate");
setTimeout(() => $span.classList.add("animate"), 50);
},
updateVModel(){
this.changeAnimation();//this line is redundant since watch.value runs anyway
this.$emit("input", this.checked);
}
}
});
new Vue({
el: "#menu",
data: {
menu: 0,
checked0: true,
checked1: false
}
});
span{
transition: 1s;
opacity: 0;
}
span.animate{
animation: notice-me .3s ease-in-out 4 alternate
}
#keyframes notice-me{
0%{
opacity: 0;
}
100%{
opacity: 1;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<div id="menu">
<div v-if="menu == 0">
<button v-on:click="menu = 1">Go to menu 1</button>
<h3>Menu 0</h3>
<checkbox v-model="checked0">checked0</checkbox>
<checkbox v-model="checked0">checked0</checkbox>
</div>
<div v-else>
<button v-on:click="menu = 0">Go to menu 0</button>
<h3>Menu 1</h3>
<checkbox v-model="checked1">checked1</checkbox>
<checkbox v-model="checked1">checked1</checkbox>
</div>
</div>
I've made my components as so whenever the value changes, either from a user click(with v-on:click) or from the variable itself changing (with watch.value), the word "changed!" blinks besides the checkbox a couple of times. Everything works fine, but the problem arises when we change menus using the "Change to menu" button and checked0 and checked1 are different values. "changed" will blink even though it shouldn't have.
This is obviously caused by vue recycling the components and using them for another variable. Since the variable's value is different from the old one, watch.value is run, triggering the animation when we'd expect it not to happen.
I did a little bit of research and found that I could give all my different checkboxes vue keys like so: <checkbox v-model="checked1" key="thing1">checked1</checkbox>, but I want to fix this elegantly and allow vue to recycle whatever it wants. There should be a way to detect if the value changed because it actually did or because of recycling.
So my question is how to I fix this problem, or how do I write my code differently to avoid it?

A key should really be associated with the conditionally-rendered (by v-for or v-if) unit. It should be bound to whatever is unique about the unit. In your case, you can use menu:
<div v-if="menu == 0" :key="menu">
<button v-on:click="menu = 1">Go to menu 1</button>
<h3>Menu 0</h3>
<checkbox v-model="checked0">checked0</checkbox>
<checkbox v-model="checked0">checked0</checkbox>
</div>
<div v-else :key="menu">
<button v-on:click="menu = 0">Go to menu 0</button>
<h3>Menu 1</h3>
<checkbox v-model="checked1">checked1</checkbox>
<checkbox v-model="checked1">checked1</checkbox>
</div>

Related

How to click on a button and click on it again to close all the content AND how to change image when you go into darkmode using HTML CSS or JS?

(I will link a code sandbox with all my code (at its latest) to be viewed if you want to)
https://codesandbox.io/s/distracted-hellman-pyej06?file=/index.html
I have two issues that I was hoping to address
Problem-1: I have a website called dirieahmed. ml (the same as the code sandbox) I have a night mode / light mode toggle, however when I click on night mode my logo (the image which is called hello.jpg and is above the face picture) will stay the same which makes sense but looks terrible because it looks like an imperfect white square. As a result, I wanted to change this. Therefore when I click on night mode I want the image to change into the night mode version of the logo I have made (it will be called hello-dark.jpg) Is there a way i can do that? I will link the appropriate code down below but if you would like to see the CSS you can view the sandbox
<div class="image1">
<img src="img/hello.jpg" alt="something" width="400" height="300">
</div>
<script async>
<--!This is my dark theme toggle-->
document.querySelector('.theme-toggle-button').addEventListener('click', () => {
document.body.classList.toggle('dark')
})
Problem-2: On my sandbox, you can view an About me, Education and Achievements and Other content elements, these elements are buttons when clicked they will drop down a content, if I click on about me then Education about me will open then close and allow Education to open. Pretty Normal and similarly for the button called "Other" but when i click on Other again to reset all the views and make it clean like when it was originally where no dropdowns exsisted that wont happen. Is there a way for me to make a javascript that will close all content from that div when clicked on it twice. (the code of CSS is in the sandbox but Html and JS will be here)
<div class="container">
<button id="one" class="click one" title="click me">About Me</button>
<div class="list-1 sec">
<h1 class="content-header-one content">Dummy text header</h1>
<p class="content-paragraph-one">Dummy text</p>
</div>
<button class="click two" title="click me">Education and Achivements</button>
<div class="list-2 sec">
<p class="content-paragraph2 content">Dummy text</p>
<ul class="content-list content">
<li>- Achivement #1</li>
<li>- Achivement #2</li>
<li>- Achivement #3</li>
</ul>
</div>
<button class="click three" title="click me" >Other</button>
<div class="list-3 sec">
<h1 class="content-header-two content">Dummy text header</h1>
<p class="content-paragraph3 content">Dummy text</p>
</div>
<script async>
// Instanciate List div's
let list1 = document.querySelector(".list-1");
let list2 = document.querySelector(".list-2");
let list3 = document.querySelector(".list-3");
// Reset's all views to the default without .newlist
const resetAllViews = () => {
list1.classList.remove("newlist");
list2.classList.remove("newlist");
list3.classList.remove("newlist");
};
// Checks to see what button is clicked and shows correct list based on input
document.addEventListener(
"click",
function (e) {
e = e || window.event;
var target = e.target;
if (target.classList.contains("one")) {
resetAllViews();
list1.classList.add("newlist");
}
if (target.classList.contains("two")) {
resetAllViews();
list2.classList.add("newlist");
}
if (target.classList.contains("three")) {
resetAllViews();
list3.classList.add("newlist");
}
}, false);
</script>
</div>
Again you can view the result I have in the sandbox (latest) and on the most recent website dirieahmed.ml for real life view
Sorry if I repeated myself a lot and any help would be appreciated though I prefer if you can show me the code as a runner snippet as I am fairly new so its a bit difficult for me to understand all vocabulary but again any help is appreciated
As for the 1st point, i think you have 2 possibilities:
Add the image as a background image with css, so you can easily toggle. Normally you show the "light" image, and if the body has the dark class then you can show the "dark" image.
The second idea is to add two <img> elements to the page and hide / show accordingly, something like:
.image-light {
display: block;
}
body.dark .image-light {
display: none;
}
.image-dark {
display: none;
}
body.dark .image-dark {
display: block;
}
As to the 2nd point i think you should to it like this:
you have a variable to hold the current open element
when you click a button you get the "value" vor that button
If the value if the same as the current open variable, then you reset the variable (maybe to null), otherwise you set the current open variable to the value of that button
Then you can have an update function. On this function you go through all the of "target" elements. If the element corresponds to the currently open variable, you open it, otherwise you close it.
Something like:
let currentlyOpen = null;
let buttons = document.querySelectorAll(".your-buttons");
let targets = document.querySelectorAll('.your-targets');
button.forEach((button) => {
button.addEventListener('click', (event) => {
const newTarget = event.target.getAttribute('your-identifier-attribute');
currentlyOpen = currentlyOpen === newTarget ? null : newTarget;
updateUI();
});
})
function updateUI() {
targets.forEach((target) => {
targetIdentifier = target.getAttribute('the-identifier-attribute');
targetIdentifier === currentlyOpen
? target.classList.add('your-class-for-open-list')
: target.classList.remove('your-class-for-open-list');
})
}
I have created an example for you:
let openContent = null;
const buttons = document.querySelectorAll('button');
const contents = document.querySelectorAll('.content');
buttons.forEach((button) => {
button.addEventListener('click', (event) => {
const newTargetId = event.target.getAttribute('target-id');
openContent = newTargetId === openContent ? null : newTargetId;
updateUI();
});
});
function updateUI() {
contents.forEach((content) => {
const contentId = content.getAttribute('id');
contentId === openContent
? content.classList.add('visible')
: content.classList.remove('visible');
});
}
.content {
display: none;
}
.content.visible {
display: block;
}
<p> click one of the buttons to show more content</p>
<button target-id="one">One</button>
<button target-id="two">Two</button>
<button target-id="three">three</button>
<p class="content" id="one">Content one</p>
<p class="content" id="two">Content two</p>
<p class="content" id="three">Content three</p>
Here is an example of doing this using two elements where one is hidden based on theme. You could make it a photo that you want to change. This way both images are loaded and you don't have to have the client wait for an image to load when themes change.
The way I accomplished this was mainly in css. The only javascript is to toggle the dark class on the page. The dark page has css variables which represent whether or not elements on the page have a display of none or block. On the actual elements which toggle, I feed those css variables into them. It is also possible to add animations or transitions to make it feel fancy, but hopefully this small demonstration satisfies your need for modifications to your own project.
const $ = str => document.querySelector(str);
const page = $(".page");
$(".theme-toggle").addEventListener("click", () => {
page.classList.toggle("dark");
});
body {
display: grid;
place-items: center;
}
.page {
--light-logo: block;
--dark-logo: none;
display: flex;
flex-direction: column;
text-align: center;
}
.page.dark {
--light-logo: none;
--dark-logo: block;
background-color: black;
color: white;
}
.logo-container {
font-size: 5rem;
}
.logo-container .dark {
display: var(--dark-logo);
}
.logo-container .light {
display: var(--light-logo);
}
<div class="page">
Hello World
<div class="logo-container">
<div class="light">🌞</div>
<div class="dark">🌑</div>
</div>
<button class="theme-toggle">🌞 / 🌑</button>
</div>
Answer 1: It is possible to simple solve by CSS, but this is not good idea for the SEO optimization. Better and most simple solution is to use 2 images and add class "hidden", where you will set for this class {display:none} in css file. Next you must modify your script to set/remove "hidden" class by the selected light/dark mode.
<div class="image1">
<img src="img/hello.jpg" alt="something" width="400" height="300">
</div>
Another better solution is modify the DOM, replace img element when mode is changed, or change the src attribute, e.g.:
document.getElementById("myImageId").src="img/hello-dark.jpg";

Vue Sortable + Draggable not working when input boxes strings are longer than the box size

I'm using Vue draggable with Sortable.js.
Pretty cool library for dragging/reordering items on a list.
I found a problem in it though, whenever I have a list of input elements and the text is longer than the input box, that specific input box does not drag and drop.
I've tried debugging it but couldn't find if the issue could be on the library or some input box event that I could override to fix it.
Here is a fiddle showing an example: https://jsfiddle.net/egmstvL7/
Snipped below. Any ideas?
var app = new Vue({
el: '#app',
data: {
myArray:["This one drags ok","This one too","Well, this one too","and this","Everything else drags except inputs that have string longer than the element size"],
message: 'Hello Vue!'
}
})
<script src="http://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<script src="http://cdn.jsdelivr.net/npm/sortablejs#1.8.4/Sortable.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.20.0/vuedraggable.umd.min.js"></script>
<div id="app">
{{ message }}
<draggable v-model="myArray" group="people" #start="drag=true" #end="drag=false">
<div v-for="element in myArray" :key="element.id">
<input type="text" v-model="element"></input>
</div>
</div>
For some reason, draggable isn't disabling text selection on the longer input, so you can disable it yourself. Do that using a CSS class and the pointer-events property:
.noselect {
pointer-events: none;
}
Use a boolean to toggle this class on all inputs:
data: () => ({
lock: false // This will toggle a class on all textboxes
...
}
Use mousedown, mouseup, and blur events to manage the toggle and apply the noselect class when lock is true:
<input type="text"
v-model="element"
#mousedown="lock = true"
#mouseup="lock = false"
#blur="lock = false"
:class="{ noselect: lock }"
/>
Here's a demo:
var app = new Vue({
el: '#app',
data: () => ({
lock: false,
drag: false,
myArray:["This one drags ok","This one too","Well, this one too","and this","Everything else drags except inputs that have string longer than the element size"],
message: 'Hello Vue!'
})
})
.noselect {
pointer-events: none;
}
<div id="app">
<draggable
v-model="myArray"
group="people"
#start="drag=true"
#end="drag=false"
>
<div v-for="element in myArray" :key="element.id">
<input type="text"
v-model="element"
#mousedown="lock = true"
#mouseup="lock = false"
#blur="lock = false"
:class="{ noselect: lock }"
/>
</div>
</draggable>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs#1.8.4/Sortable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Vue.Draggable/2.20.0/vuedraggable.umd.min.js"></script>
I managed to solve it using a simple class that sortable automatically adds to dragging elements, here's the simplest solution but thanks #Dan for the clarification.
.sortable-chosen > input{
pointer-events: none;
}

VUE: Disable buttons until animation ends

how to prevent click when animation is in progress with VUE?
<button v-model="show" #click="show === 'showFirst'">click me first</button>
<button v-model="show" #click="show === 'showSecond'">click me second</button>
<transition :enter-active-class="'animated fadeIn'" :leave-active-class="'animated fadeOut'">
<div class="first" key="1" v-if="showFirst">First Div</div>
<div class="second" key="2" v-if="showSecond">Second Div</div>
</transition>
VUE
export default {
data: function() {
return: {
show: "showFirst"
}
}
}
After clicking on the first button, both will be deactivated until the animation ends. Is it possible either in this way or in another way?
Now when I click both buttons alternately, the animations interrupt each other.
You can use <transition></transition> JS hooks.(https://v2.vuejs.org/v2/guide/transitions.html#JavaScript-Hooks)
<transition #before-enter="disabled=true" #after-leave="disabled=false">
<div class="first" key="1" v-if="showFirst">First Div</div>
<div class="second" key="2" v-if="showSecond">Second Div</div>
</transition>
listen to transition end with vanilla js
const transition = document.querySelector('.transition');
transition.addEventListener('transitionend', () => {
console.log('Transition ended');
});
There are some things I don't really get in your code though. Like why the buttons have v-models. What are you trying to perform two-way data binding for? I think just having the buttons trigger events is enough.
And why your click events are comparing and not assigning.
You could use the disabled prop for the buttons too, so they're disabled respective to a boolean you're manipulating.
So probably it should be something like this:
<button #click="isActivated" :disabled="!isEnabled">click me first</button>
<button #click="isActivated" :disabled="!isEnabled">click me second</button>
<transition :enter-active-class="'animated fadeIn'" :leave-active-class="'animated fadeOut'">
<div class="first" key="1" v-if="show === 'showFirst'">First Div</div>
<div class="second" key="2" v-if="show === 'showSecond'">Second Div</div>
</transition>
Then depending on the timing of your animations, you can have a disabled function that you pass as a prop to the button element.
Like this
data() {
return {
show: 'showFirst',
isEnabled: true,
}
},
methods: {
isActivated(){
//first starts off as false
this.show === 'showFirst' ? this.show = 'showSecond' : this.show = 'showFirst'
this.disableButtons();
},
disableButtons(){
//disable the button then enable after the number of seconds of the animation
this.isEnabled = false
setTimeout(() => {
this.isEnabled = true
}, noOfSecondsOfAnimation)
}
}

How can i listen for an event from a sub component in vue?

I have 3 components
Stage
StageComponent
StageElementComponent
the Stage contains the default data and the basic control of all 3 components.
the StageComponent i filter the StageElementComponent elements, and have some design, validations and user interactions in it.
the StageElementComponent display and element based on the user choise.
Things to take in account
I want to know if it is possible to emit an event from StageElementComponent and listen to it in the Stage component? like
<template>
//StageElementComponent
<div class="element d-flex flex-column justify-content-center"
#dblclick="clear">
<i class="material-icons" v-if="element" :class="[`bg-${element.type.color}`]">{{ element.type.icon }}</i>
</div>
</template>
<script>
export default {
props : {
element : {
type : Object,
required : true
}
},
methods : {
clear : function($event) {
this.$emit('clear', {
index : this.element.index
})
}
}
}
</script>
<style scoped lang="scss">
.element {
cursor: pointer;
}
</style>
and in the Stage component listen to it like
<v-stage id="ticktr" class="section center"
:rows="height"
:columns="width"
:elements="elements"
#clear="clearElement"></v-stage>
I do need these 3 components.
I don't want to listen and emit in the StageComponent as i would be repeating myself
I don't want to use $root.$emit.
It is possible to do this? how?

How to iterate through DOM elements in react and add/remove classes?

I am beginner to react and I am unable to iterate through div elements. Each of these div elements (shown in the code) have a common className="step" initially but once the button Next is clicked I want 1st div to have className="step current" .
Once Next is clicked again 1st div element should have className="step done"(remove current and append done) and 2nd div element would have classname="step current"
I was able to toggle the className from "step" to "step current" in the following code but I am facing difficulty to traverse the div elements and add or remove the class.
class NavBar extends React.Component{
state = {
addClass : false
}
handleClick=()=>{
this.setState({addClass: !this.state.addClass});
}
render(){
let arrowClass = ["step"];
if(this.state.addClass){
arrowClass.push("current");
}
return(
<div id="navbar-div">
<div className="arrow-steps clearfix">
1. <div className={arrowClass.join(' ')}>
<span> Step1</span>
</div>
2. <div className={arrowClass.join(' ')}>
<span>Step2</span>
</div>
3. <div className={arrowClass.join(' ')}>
<span> Step3</span>
</div>
4. <div className={arrowClass.join(' ')}>
<span>Step4</span>
</div>
5. <div className={arrowClass.join(' ')}>
<span>Step5</span>
</div>
</div>
<div className="nav clearfix">
Previous
<a href="#" className="next pull-right" onClick={this.handleClick}>Next</a>
</div>
</div>
);
}
}
ReactDOM.render(
<NavBar />,
document.getElementById("root")
);
.current {
font-weight: bold;
}
.done {
color: #aaa;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.2/umd/react-dom.production.min.js"></script>
The classes step, current and done are defined in the css file.
Rather than writing the steps explicitly, put them in an array, and then remember where you are within that array with an indexing variable. Here's the minimal-changes approach to doing that with your code (see comments):
class NavBar extends React.Component{
state = {
steps: ["Step1", "Step2", "Step3", "Step4"],
current: 0 // <== Step1 is the current step
}
prevClick = () => {
// Move to previous step, note we use the callback version of setState,
// see https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
this.setState(({current}) => ({current: Math.max(0, current - 1)}));
}
nextClick = () => {
// Move to next step
this.setState(({steps, current}) => ({current: Math.min(steps.length - 1, current + 1)}));
}
render() {
// Get the steps and the current index
const {current, steps} = this.state;
// Render them, checking the position of the step (`index`) relative to `current`
// and outputting the relevant class name.
// I changed your `div`s to an ordered list so we get automatic numbering
return (
<div id="navbar-div">
<div>{current}</div>
<ol className="arrow-steps clearfix">
{steps.map((step, index) =>
<li key={index} className={`step ${index < current ? 'done' : index == current ? 'current' : ''}`}>{step}</li>
)}
</ol>
<div className="nav clearfix">
<a href="#" className="prev" onClick={this.prevClick}>Previous</a>
<a href="#" className="next pull-right" onClick={this.nextClick}>Next</a>
</div>
</div>
);
}
}
ReactDOM.render(
<NavBar />,
document.getElementById("root")
);
.current {
font-weight: bold;
}
.done {
color: #aaa;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.4.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.2/umd/react-dom.production.min.js"></script>
Obviously, that just shows the basic approach, you'll want to modify it. If you have any information other than just the step text, you might make the array entries objects rather than just strings.
(Note: Using index as the key on those lis is only valid if you don't add/remove entries in the array of steps.)
Side note: As Murali Krishna pointed out, you had class rather than className on the div containing the previous/next links and on those links; I've changed those to className above.
Although you can do that but there's a more React'ish way. That is to use state to store the class you wanna add. So when you change the state (using setState) it will auto re-render and set the class.
Eg.
class NavBar extends React.Component{
state = {
status: "current"
}
handleClick=()=>{
this.setState(prevState=>{
if(prevState.status === 'current'){
return {status:"done"}
}else{
return {status:"current"}
}
})
}
render(){
return <button className={"step " + this.state.status} onClick={this.handleClick}>Hello</button>
}

Categories

Resources