Binding method to an option in a component in vue.js - vue.js

I'm pretty new to Vue and are using a litebox component which I want to customize a bit so the lightbox gallery starts on different images depending on which button is pressed. I have succeeded to solve this by using a click event which I'm binding to an option in the litebox component. Since I am new to Vue. I'm just wondering if this is a good way of solving something like this or if there is a better way?
<template>
<div id>
<button type="button" #click="show(); start1();">Show Litebox start 1</button>
<button type="button" #click="show(); start2();">Show Litebox start 2</button>
<vue-litebox v-if="showLitebox" :startAt="start" :items="images" #close="hide"></vue-litebox>
</div>
</template>
<script>
import VueLitebox from "vue-litebox";
export default {
components: { VueLitebox },
data() {
return {
images: [
"https://placekitten.com/400/400",
"https://placekitten.com/400/401",
{
title: "My image title",
src: "https://placekitten.com/400/402"
}
],
showLitebox: false,
start: 0
};
},
methods: {
show() {
this.showLitebox = true;
},
hide() {
this.showLitebox = false;
},
start1() {
this.start = 1
},
start2() {
this.start = 2
}
}
};
</script>
Here is the code on code sandbox:
https://codesandbox.io/s/9ok4y6lopo?fontsize=12

Templates should be as logic-free as possible, besides, there is no need to chain methods this way because you can always pass parameters to methods, like this:
// in your template
<button
type="button"
#click="show(1)"
>
Show Litebox start 1
</button>
// in methods section
show (start = 1) { // defaults to 1
this.show = true;
this.start = start;
}
By the way, it seems like v-show would be a better choice than v-if for vue-litebox component (see documentation).

Related

Vue.js : Range slider with two handles

I want to create a vue js components where it contains a range slider of hours with two handles.
I use vue3 + vite.js
I tried this code to implement the components but when I drag one of handles I have an error
Code :
this is the template :
<template>
<div>
<input type="range" ref="rangeInput" v-model="rangeValue" #input="updateRange"/>
<div class="range-slider">
<div class="handle" :style="{left: leftHandle + '%'}" #mousedown="startHandleDrag(1)">
{{ formatHour(rangeValue[0]) }}
</div>
<div class="handle" :style="{left: rightHandle + '%'}" #mousedown="startHandleDrag(2)">
{{ formatHour(rangeValue[1]) }}
</div>
</div>
</div>
</template>
and this is the script :
<script>
export default {
data() {
return {
rangeValue: [8, 18],
handleDragging: 0
};
},
computed: {
leftHandle() {
return this.rangeValue[0];
},
rightHandle() {
return this.rangeValue[1];
}
},
methods: {
updateRange(event) {
const value = event.target.value;
const range = this.rangeValue;
if (this.handleDragging === 1) {
range[0] = value[0];
} else if (this.handleDragging === 2) {
range[1] = value[1];
} else {
range[0] = value[0];
range[1] = value[1];
}
this.rangeValue = range;
},
startHandleDrag(handle) {
this.handleDragging = handle;
document.addEventListener("mouseup", this.stopHandleDrag);
document.addEventListener("mousemove", this.updateRange);
},
stopHandleDrag() {
this.handleDragging = 0;
document.removeEventListener("mouseup", this.stopHandleDrag);
document.removeEventListener("mousemove", this.updateRange);
},
formatHour(value) {
return value + ":00";
}
}
};
</script>
Error :
any ideas to solve it !!!
In your startHandleDrag() and stopHandleDrag(), you bind updateRange() to the mousemove event:
document.addEventListener("mousemove", this.updateRange);
There are two issues with that:
The target of the mousemove event is the element under the cursor. This can be any element, and unless it happens to be an input, it will not have a value attribute (and if it does, it will not hold an array). If you really want to use the "mousemove" event, use the cursor coordinates like pageX or pageX.
You bind it as a function pointer (addEventListener("mousemove", this.updateRange)), and when called from the listener, this will refer to element.target. To avoid this, either use an arrow function (addEventListener("mousemove", (e) => this.updateRange(e))) or bind this (addEventListener("mousemove", this.updateRange.bind(this))).
I don't fully understand what you want to do with the handles, but my guess is that adding and removing listeners is a workaround, and you actually want to make them draggable? If so, have a look at the drag event. Hope that helps!

Passing data from child component to parent and then to another child not working on page load but works after minor potentially unrelated change

I am new to Vuejs and come across this bug which I have no idea what I have done wrong. I am not receiving any console errors. It doesn't work on initial page load but it seems to work after I comment something out (or make a minor change). It will still then continue to work if I reverse the changes I just made and put it back to the original code. But once again on a fresh page load it won't work.
The issue: I am making a to do list and on page load when I add new tasks through the input field, the list does not appear on the page like it should be. I also console log the data array for this and it shows it is getting added to the array but is not getting rendered to the page. No console errors. In my code I will comment out some other data property (there are 2 additional ones below todosList in the TodoList.vue file that are currently not being used yet) and save and then the tasks will automatically appear on the page. So I think oh ok that might be the issue so with this new minor change I decide to refresh the page to see if it works as expected. Nope it doesn't so I then uncomment out what I previously commented out and save and the list appears again. But once again if I refresh the page it doesn't work. It only seems to be if I make a change inside the data function in the TodoList.vue file.
Additional info: The data is stored in the parent todos[] (App.vue), updated/pushed to array in a child (TodoCreate.vue) and sent back to the parent using $emit. This data is then sent through to another child (TodoList.vue) using props so that it can be rendered on the page.
Wondering if there is something that is not quite right in my code which is causing this to bug out like that. I will include everything in case it is something that looks unrelated to me but could be causing it.
Here is also a link to a code sandbox where the issue can be replicated by following the instructions on the page https://codesandbox.io/s/adding-new-todo-not-working-properly-jwwex?file=/src/components/TodoList.vue
main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
App.vue
<template>
<div :class="currentMode">
<the-header #modeToggled="updateMode($event)"></the-header>
<main>
<todo-create #addedTodos="updateTodos"></todo-create>
<todo-list :todos="todos"></todo-list>
</main>
</div>
</template>
<script>
import TheHeader from './components/TheHeader.vue';
import TodoCreate from './components/TodoCreate.vue';
import TodoList from './components/TodoList.vue';
export default {
name: 'App',
components: {
TheHeader,
TodoCreate,
TodoList,
},
data() {
return {
currentMode: {
dark_mode: true,
light_mode: false
},
todos: [],
}
},
methods: {
updateMode(mode) {
this.currentMode = mode;
},
updateTodos(data) {
this.todos = data;
console.log(this.todos);
},
toggleCompleted() {
}
},
// provide() {
// return {
// todos: this.todos,
// };
// }
}
</script>
TheHeader.vue
<template>
<h1>To-do App</h1>
<div>
<label for="toggle-mode" aria-label="Toggle light and dark mode"></label>
<input type="checkbox" id="toggle-mode" #change="toggleMode">
</div>
</template>
<script>
export default {
emits: ['modeToggled'],
data() {
return {
toggleState: false,
}
},
methods: {
toggleMode() {
this.toggleState = !this.toggleState;
this.$emit('modeToggled', this.modeClasses);
}
},
computed: {
modeClasses() {
return {
dark_mode: !this.toggleState,
light_mode: this.toggleState
}
}
}
}
</script>
TodoCreate.vue
<template>
<div>
<label for="newtodo" class="sr-only">Create new to do</label>
<input type="text" id="newtodo" placeholder="Create a new todo..." v-model="todoval" v-on:keyup.enter="addTodo" >
</div>
</template>
<script>
export default {
emits: ['addedTodos'],
data() {
return {
todoval: '',
taskNumber: 0,
todos: [],
};
},
methods: {
addTodo() {
const val = this.todoval;
const taskNumber = this.taskNumber;
this.todos.push({ taskID: taskNumber, value: val, complete : 'not-completed'});
this.todoval = '';
this.taskNumber++;
console.log(this.todos);
this.$emit('addedTodos', this.todos);
},
}
}
</script>
TodoList.vue
<template>
<ul class="todo-items" :class="filterClass">
<li class="drop-zone" v-for="(listItem, index) in todosList" :class="listItem.complete" :key="listItem.taskID"
#drop='onDrop($event, index)'
#dragover.prevent
#dragenter.prevent>
<div class="drag-el" draggable="true"
#dragstart='startDrag($event, index)'>
<label :for="'checkbox-'+index" :aria-label="'Mark task ' + listItem.value + ' as completed'"></label>
<input type="checkbox" :id="'checkbox-'+index" #change="toggleCompleted(index, listItem.value, listItem.complete, listItem.taskID)">
<input type="text" disabled :value="listItem.value">
<img src="../assets/icon-cross.svg" #click="removeTask(index)">
</div>
</li>
</ul>
</template>
<script>
export default {
props: {
todos: Object,
filterClass: String
},
// inject: ['todos'],
data() {
return {
todosList: this.todos,
// completedTodos: [],
// activeTodos: [],
};
},
// watch: {
// todosList(data) {
// data.filter(function(todo) {
// if(todo.completed == 'completed') {
// completedTodos.push(todos);
// }
// });
// }
// },
methods: {
startDrag: (evt, item) => {
evt.dataTransfer.dropEffect = 'move'
evt.dataTransfer.effectAllowed = 'move'
evt.dataTransfer.setData('itemID', item)
},
onDrop (evt, list) {
const itemID = evt.dataTransfer.getData('itemID');
const movedData = this.todosList[itemID];
this.todosList.splice(itemID,1);
this.todosList.splice(list,0, movedData);
},
toggleCompleted() {
// still need to write this method
},
removeTask() {
// still need to write this method
}
}
}
</script>

Don't update component until image loaded

I have an CurrentItem component, which shows an image and a description of some item. The item regularly changes based on events from the server. The problem I'm having is that when it does, the new item's image takes a few moments to load during which time the component just has a big blank space where the image is supposed to go, which does not look good.
What I would prefer instead is to keep showing the old item until the new item's image is loaded, and only at that point update the component. Is there a simple way to achieve that?
Some example code:
For the purposes of the question, you can think of an item as a simple JSON structure:
{
"id": "ABC123",
"description": "A fine yellow specimen",
"image": "//example.com/images/ABC123.jpeg"
}
The events from the server JSON over a WebSocket, and look something like this:
{ "nextItemId": "ABC124" }
When we receive one of those, we send a GET request to fetch the next item at //example.com/items/ABC124.json.
This is my own best shot at a solution.
Basically my idea is to have another copy of <CurrentItem /> in the DOM which has display:none; and which holds the item that is about to be the current. Then, when the image in that component is loaded, we update our actual CurrentItem. Something like this:
<template>
<CurrentItem
:key="currentItemId"
:item="currentItem"
/>
<CurrentItem
:key="aboutToBecomeCurrentItemId"
:item="aboutToBecomeCurrentItem"
#imageLoaded="onImageReady"
style="display:none;"
/>
</template>
<script>
// ...
export default {
// ...
data() {
return {
currentItem = null,
aboutToBecomeCurrentItem = null
},
methods: {
onItemUpdated(newItem) {
this.aboutToBecomeCurrentItem = newItem;
},
onImageReady() {
this.currentItem = this.aboutToBecomeCurrentItem;
}
}
}
// ...
}
</script>
I think this would work, but I would love to know it if there are simpler or more idiomatic ways to achieve the same.
Preload the image and wait for its onload event to fire before setting the item:
methods: {
onReceived(newItem) {
const image = new Image(); // Create new empty image
image.onload = () => { // CALLBACK: Will fire when image is done loading
this.item = newItem; // Will set the current item
this.isLoading = false; // Will stop animation
}
this.isLoading = true; // Start animation
image.src = newItem.image; // Begin loading
}
}
Once it's finished, the current item will be set. Here's a demo with a large image:
Vue.component('item', {
props: ['item'],
template: `
<div>
<div>ITEM (id: {{ item.id }})</div>
<div>{{ item.description }}</div>
<div><img :src="item.image" ref="img" width="100" height="100" /></div>
</div>
`
})
new Vue({
el: "#app",
data() {
return {
isLoading: false,
item: {
"id": "ABC123",
"description": "A fine yellow specimen",
"image": "https://cdn.iconscout.com/icon/free/png-256/google-470-675827.png"
}
}
},
methods: {
simulateEvent() {
this.onReceived({
"id": "ZYX987",
"description": "A funky jaundiced specimen",
"image": "https://upload.wikimedia.org/wikipedia/commons/f/fa/Very_Large_Array_in_New_Mexico.jpg"
});
},
onReceived(newItem) {
this.isLoading = true;
const image = new Image();
image.onload = () => {
this.item = newItem;
this.isLoading = false;
}
image.src = newItem.image;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<item :item="item"></item>
<!-- Simulating events -->
<button #click="simulateEvent">
Simulate event
</button>
<span v-if="isLoading">[ Loading... ]</span>
</div>
you can set a time out function until the image loads
.. codes to load new image
setTimeout(function() {
... your old image codes
... 5000 means 5Seconds
},5000);
.. codes to display new image

How you do you call a method once rendering is done in Vue?

I have am building a Vue app that includes a QuillJS editor in a tab. I have a simple setTab(tabName) Vue method that shows/hides tabs with the v-if directive.
methods: {
setTab: function (tabName) {
this.view = tabName;
if(tabName === 'compose') {
var editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
}
}
}
My tab is basically like this:
<div id="composer" v-if="tabName === 'compose'">
<!-- toolbar container -->
<div id="toolbar">
<button class="ql-bold">Bold</button>
<button class="ql-italic">Italic</button>
</div>
<!-- editor container -->
<div id="editor">
<p>Hello World!</p>
</div>
</div>
Currently, I'm getting an error because the #editor element does not yet exist when I am calling new Quill(...). How do I delay that QuillJS initialization on the page so that it doesn't happen until after the #editor is already there?
Use mounted hook.
mounted: function () {
// Code that will run only after the
// entire view has been rendered
}
Use this.$nextTick() to defer a callback to be executed after the next DOM update cycle (e.g., after changing a data property that causes a render-update).
For example, you could do this:
methods: {
setTab: function (tabName) {
this.view = tabName;
if(tabName === 'compose') {
this.$nextTick(() => {
var editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
})
}
}
}
A clean way to do this is not to rely on selectors but make Quill editor a self-contained component:
<template>
<div class="quill-editor">
<!-- toolbar container -->
<div ref="toolbar">
<button class="ql-bold">Bold</button>
<button class="ql-italic">Italic</button>
</div>
<!-- editor container -->
<div ref="editor">
<p>Hello World!</p>
</div>
</div>
</template>
<script>
...
name: "QuillEditor",
mounted() {
this.quill = new Quill(this.$refs.editor, {
modules: { toolbar: this.$refs.toolbar },
theme: 'snow'
});
}
...
</script>

How to run a function in Vue.js when state changes

I'm looking to run a function when the state changes in my Vue app.
In my component I'm able to get the boolean state of isOpen. I'm looking to run a function that adds focus to my form input when the modal opens and isOpen is set to true. I've tried using a watcher but with no luck. I'm opening my modal by calling :class="{ 'is-open': search.isOpen }" in the html and showing it via css. Any help would be most appreciated.
data() {
return {
isFocussed: this.$store.state.search,
//checks the state of isOpen?
}
},
computed: {
search() { return this.$store.state.search },
},
watch: {
isFocussed() {
this.formfocus()
},
},
methods: {
formfocus() {
document.getElementById('search').focus()
},
please check my snippet which shows the good way to work in Vue.js, you can work with refs which is very helpful instead of document.getElementById()
new Vue({
el: '#app',
data: {
isOpen: false,
},
computed: {
},
watch: {
isOpen(){
if(this.isOpen){
this.$nextTick( function () {
this.formfocus();
}
);
}
}
},
methods: {
formfocus(){
this.$refs.search.focus();
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.js"></script>
<div id="app">
<button v-on:click="isOpen = !isOpen">show modal</button>
<div v-if="isOpen">
<input ref="search" type="text" placeholder="search">
</div>
</div>
EDIT: i have added a conditional if on the watch, i hope this solves the problem
I am not sure what your template looks like but here is how I set focus on a conditional element.
element
<input type="text" class="search-input" v-model="search" :class="{'collapsed-search': hideInput}" placeholder="Enter keyword" ref="search">
notice the ref="search" on the input.
here is the method when the input condition is true
toggleSearch() {
this.hideInput = !this.hideInput
this.search = ''
setTimeout(() => {
this.$refs.search.focus()
}, 1000)
}
this.$refs.search.focus() is called after the element has been fully created which is the purpose of the setTimeout