Is there a way to dynamically insert click events in Vue.js? - javascript

Trying to achieve inserting some computed methods onto an element depending on mobile viewports only. Here's a basic gist of what I'm working with:
<a class="nav-link float-left p-x-y-16" v-bind:class={active:isCurrentTopicId(t.id)} #click="onTopicClicked($event, m, t)" href="#">{{t.title}}</a>
<script>
export default {
data() {
return {
isClosed: false
}
},
computed: {
toggleMenu() {
return {
isClosed: this.isClosed
}
}
},
watch: {
browserWidth(prevWidth, newWidth) {
console.log('width changed from ' + newWidth + ' to ' + prevWidth);
},
mounted() {
var that = this;
this.$nextTick(function() {
window.addEventListener('resize', function(e) {
that.browserWidth = window.innerWidth;
if(that.browserWidth > 824) {
console.log('Desktop View');
} else {
console.log('Mobile View');
}
})
})
}
}
</script>
I would like to try to use the resize event to determine browser width so that I can dynamically insert the computed function onto that <a> tag

You could either provide two different elements (one for desktop and another for mobile) as stated by Karthikeyan, or conditionally add click event to that element:
v-on="isMobileView ? { mouseover: onTopicClicked($event, m, t) } : {}"

You can add a data that says if the view is mobile or not and use v-if , v-else and have the #click only added to the v-if="isMobileView"
<a v-if="isMobileView" class="nav-link float-left p-x-y-16" v-bind:class={active:isCurrentTopicId(t.id)} #click="onTopicClicked($event, m, t)" href="#">{{t.title}}</a>
<a v-else class="nav-link float-left p-x-y-16" v-bind:class={active:isCurrentTopicId(t.id)} href="#">{{t.title}}</a>
<script>
export default {
data() {
return {
isClosed: false,
isMobileView: false
}
},
computed: {
toggleMenu() {
return {
isClosed: this.isClosed
}
}
},
watch: {
browserWidth(prevWidth, newWidth) {
console.log('width changed from ' + newWidth + ' to ' + prevWidth);
},
mounted() {
var that = this;
function checkIfMobileView() {
that.isMobileView = window.innerWidth <= 824;
}
this.$nextTick(function() {
window.addEventListener('resize', checkIfMobileView);
});
checkIfMobileView();
}
}
</script>

Related

VueJS detecting if a button was clicked in Watch method

I am creating undo/redo functionality in VueJS. I watch the settings and add a new element to an array of changes when the settings change. I also have a method for undo when the undo button is clicked.
However, when the button is clicked and the last setting is reverted, the settings are changed and the watch is fired again.
How can I prevent a new element being added to the array of changes if the settings changed but it was because the Undo button was clicked?
(function () {
var Admin = {};
Admin.init = function () {
};
var appData = {
settings: {
has_border: true,
leave_reviews: true,
has_questions: true
},
mutations: [],
mutationIndex: null,
undoDisabled: true,
redoDisabled: true
};
var app = new Vue({
el: '#app',
data: appData,
methods: {
undo: function() {
if (this.mutations[this.mutationIndex - 1]) {
let settings = JSON.parse(this.mutations[this.mutationIndex - 1]);
this.settings = settings;
this.mutationIndex = this.mutations.length - 1;
console.log (settings);
}
},
redo: function() {
}
},
computed: {
border_class: {
get: function () {
return this.settings.has_border ? ' rp-pwb' : ''
}
},
undo_class: {
get: function () {
return this.undoDisabled ? ' disabled' : ''
}
},
redo_class: {
get: function () {
return this.redoDisabled ? ' disabled' : ''
}
}
},
watch: {
undoDisabled: function () {
return this.mutations.length;
},
redoDisabled: function () {
return this.mutations.length;
},
settings: {
handler: function () {
let mutation = JSON.stringify(this.settings),
prevMutation = JSON.stringify(this.mutations[this.mutations.length-1]);
if (mutation !== prevMutation) {
this.mutations.push(mutation);
this.mutationIndex = this.mutations.length - 1;
this.undoDisabled = false;
}
},
deep: true
}
}
});
Admin.init();
})();
Since you make the changes with a button click, you can create a method to achieve your goal instead of using watchers.
methods: {
settings() {
// call this method from undo and redo methods if the conditions are met.
// move the watcher code here.
}
}
BTW,
If you don't use setter in computed properties, you don't need getters, so that is enough:
border_class() {
return this.settings.has_border ? ' rp-pwb' : ''
},
These watchers codes look belong to computed:
undoDisabled() {
return this.mutations.length;
},
redoDisabled() {
return this.mutations.length;
},

Vue js returning wrong element height

I need to get the height of an image/element, this is what I did:
mounted() {
this.infoHeight = this.$refs.info.clientHeight + 'px';
}
When I save then on hot reload it works, it gets the correct height but when I refresh the page it returns a smaller/wrong value. I also tried it on created() and it's the same. On other situations it doesn't even return anything.
UPDATE (Temporary solution?)
mounted() {
setTimeout(() => this.infoHeight = this.$refs.info.clientHeight + 'px', 100);
}
I also tried using window.addEventListener('load', () => //todo) but on some components it worked and on others it didn't.
You can do this now in a way cleaner fashion using a ResizeObserver.
data: () => ({
infoHeight: 0,
resizeObserver: null
}),
mounted() {
this.resizeObserver = new ResizeObserver(this.onResize)
this.resizeObserver.observe(this.$refs.info)
this.onResize()
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.info)
},
methods: {
onResize() {
this.infoHeight = this.$refs.info.clientHeight + 'px'
}
}
Try with $nextTick which will execute after DOM update.
mounted() {
this.$nextTick(() => { this.infoHeight = this.$refs.info.clientHeight + 'px' });
}
You could use this.$watch with immediate:true option :
mounted () {
this.$watch(
() => {
return this.$refs.info
},
(val) => {
this.infoHeight = this.$refs.info.clientHeight + 'px'
},
{
immediate:true,
deep:true
}
)
}
The above solution works only in the initial mount, the following one use MutationObserver
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
infoHeight: 0,
observer: null,
img: "https://images.ctfassets.net/hrltx12pl8hq/6TOyJZTDnuutGpSMYcFlfZ/4dfab047c1d94bbefb0f9325c54e08a2/01-nature_668593321.jpg?fit=fill&w=480&h=270"
}),
mounted() {
const config = {
attributes: true,
childList: true,
subtree: true
};
this.observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation) {
this.infoHeight = this.$refs.info.clientHeight + 'px'
console.log(" changed ", this.$refs.info.clientHeight)
}
});
});
this.observer.observe(this.$refs.info, config);
},
methods: {
changeImg() {
this.img = "https://i.pinimg.com/originals/a7/3d/6e/a73d6e4ac85c6a822841e449b24c78e1.jpg"
},
}
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app" class="container">
<p>{{infoHeight}}</p>
<button class="btn btn-primary" #click="changeImg">Change image</button>
<div ref="info">
<img :src="img" alt="image" />
</div>
</div>

Component props and data not being updated by parent

I made a codepen of my issue here https://codepen.io/stevemr/pen/VNQbYe
I have a root Vue instance which maintains the props for a component, VideoPlayer. My root instance has a method called setVideo, which is just assigning some dummy values right now.
Here's the object I'm using in the data of the root instance:
video: {
drive: '',
filename: '',
mediaType: '',
},
Here's the setVideo function:
setVideo: function() {
// Get the drive, filename, and mediaType
this.video.drive = 'hdd1';
this.video.filename = 'game-of-thrones_s01e04.mp4';
this.video.mediaType = 'show';
// Hide all modals and trigger the display of the video player
Event.trigger('hideModal');
Event.trigger('displayVideoPlayer');
},
The Event class is just a wrapper for basic Vue events:
window.Event = new class {
constructor() {
this.vue = new Vue();
}
trigger(event, data = null) {
this.vue.$emit(event, data);
}
listen(event, callback) {
this.vue.$on(event, callback);
}
};
Here's the DOM where my VideoPlayer component is initialized:
<video-player
v-bind:drive="video.drive"
v-bind:filename="video.filename"
v-bind:media-type="video.mediaType"
></video-player>
And finally, here's my VideoPlayer component:
<template>
<div>
<div id="movie-container">
<div
class="video-loader top-most"
v-if="showVideoPlayer && !loaded"
></div>
<video
id="video-player"
ref="video"
v-if="showVideoPlayer && src !== ''"
class="top-most"
v-bind:class="{ hidden: !loaded }"
v-on:click="togglePlay"
controls
autoplay
>
<source v-bind:src="src" v-bind:type="videoType"></source>
</video>
</div>
<div id="time-range-container" v-if="showTimeRange">
<input
id="time-range"
ref="timeRange"
type="range"
min="0"
v-bind:max="duration"
step="30"
v-model:value="currentTime"
/>
</div>
</div>
</template>
<script>
export default {
props: [
'drive',
'filename',
'mediaType',
],
data() {
return {
currentTime: 0,
duration: 0,
loaded: false,
showTimeRange: false,
showVideoPlayer: false,
}
},
computed: {
src: function() {
if(this.filename !== '') {
return
'/video/' + this.drive +
'/' + this.mediaType +
's/' + this.filename;
}
return '';
},
videoType: function() {
var ext = this.filename.split('.')[1];
var type = '';
switch(ext) {
case 'mk4':
case 'm4v':
type = 'webm';
break;
case 'avi':
type = 'ogg';
break;
default:
type = ext;
}
return 'video/' + type;
},
},
created() {
Event.listen('displayVideoPlayer', this.display);
},
methods: {
display: function() {
if(this.src === '') {
return;
}
this.showVideoPlayer = true;
this.loaded = false;
var self = this;
setTimeout(function() {
var interval = setInterval(function() {
var video = self.$refs.video;
if(video.readyState > 0) {
self.loaded = true;
self.duration = Math.round(video.duration);
self.currentTime = video.currentTime;
clearInterval(interval);
}
}, 500);
}, 800);
},
togglePlay: function() {
var video = this.$refs.video;
if(video.paused) {
video.play();
}
if(!video.paused) {
video.pause();
}
},
},
}
</script>
When setVideo is called it should set the VideoPlayer component's props to the dummy values and then the video player should be displayed. But instead when the displayVideoPlayer event is fired, the component props are still set to their default values (empty strings). Most importantly, the src computed property is not being updated before the display method is called, so the display function immediately returns without doing anything.
It's like my component's props and data aren't being updated, even though I can see with the dev tools that they are. It's like it's just not happening fast enough or something.
I've tried making src part of the component's data and setting it in the display function with another function, setSrc. But the same thing happened.
I've also tried moving Event.listen('displayVideoPlayer', this.display); into mounted() instead of created(), also did not fix anything.
If you look at the codepen, the first time you click the button to trigger the setVideo function, the video player component should be displayed, instead it takes 2 clicks.
It seems the problem is a race condition between Vue updates the value and You call display method:
display: function() {
console.log(this.src) // ""
setTimeout(() => console.log(this.src)) // "/video/hdd1/shows/game-of-thrones_s01e04.mp4"
if(this.src === '') {
return
}
This mean you call display method before the value is update.
One way to solve you is add some delay before your call display method:
setVideo: function() {
this.video.drive = 'hdd1'
this.video.filename = 'game-of-thrones_s01e04.mp4'
this.video.mediaType = 'show'
setTimeout(() => {
Event.trigger('displayVideoPlayer')
})
But I think this might get more problems in the future. If you want to rely on props then you should use watcher pattern instead:
watch: {
src (src) {
if(src === '') {
return
}
// ... display
}
}
Or pass those values through your event not on props like:
Event.trigger('displayVideoPlayer', this.video)

Noticeable lag between loading of video elements in a stack

I am working on a small scale app that displays videos in multiple ways using a video-player component.
Currently I am implementing a stack-list, which is a container that holds video-stack components, and each stack contains one or more video-player components.
While the correct videos are loaded from the DOM, there is a noticeable multi-second lag (in terms of keyboard response) which seems to be related to the ending of the currently played video and the fetching of the next video in the stack.
How can I get rid of this lag? Videos are able to be toggled/selected via mouse hovers or WASD keyboard commands (A: previous, D: next), and the lag can cause a delay in keyboard inputs being registered.
video-stack.hbs
{{video-player highlightedStyle=(string-append stackStyle borderStyle)
looping=(is-single-video videos) videoPos=selectedVidPos
isMuted=(if (video-selected key selectedStackIndex) isMuted true)
url=(if curVideo.teaser.isUrl curVideo.teaser.fileIdentifier
(make-local-url modelIdentifier curVideo.teaser.fileIdentifier))
onClickCallback=(action 'stackClicked')
onHoverCallback=(action 'stackHovered')
onEndedCallback=(action 'getNextVid')}}
video-stack.js
import Ember from 'ember';
export default Ember.Component.extend({
selectedVidPos: 0,
selectedStackIndex: 0,
stackStyle: '',
playerSize: '',
isMuted: true,
init() {
this._super(...arguments);
switch(this.get('videos').length){
case 1:
break;
case 2:
this.set('stackStyle', 'vid-shadows--2');
break;
case 3:
this.set('stackStyle', 'vid-shadows--3');
break;
case 4:
this.set('stackStyle', 'vid-shadows--4');
break;
default:
this.set('stackStyle', 'vid-shadows--4');
break;
}
},
curVideo: Ember.computed('videos', 'selectedVidPos', function () {
return this.get('videos')[this.get('selectedVidPos')];
}),
actions: {
stackClicked() {
this.get('onClickCallback') (this.get('videos'), this.get('selectedVidPos'));
},
getNextVid() {
let arrayLength = this.get('videos').length;
//check if there is only 1 video in the stack
if (arrayLength === 1) {
return;
}
let curArrayPos = parseInt(this.get('selectedVidPos'));
this.set('selectedVidPos', (curArrayPos + 1) % arrayLength);
},
stackHovered() {
this.get('onHoverCallback') (this.get('videos'), this.get('selectedStackIndex'));
}
}
});
video-player.hbs
<video oncanplay={{action 'play'}} looping=true
onended={{action 'ended'}} src={{url}}
class="video-player__video {{highlightedStyle}} {{if playing '' 'video-
player__darken'}}" muted={{muted}} />
video-player.js
import Ember from 'ember';
export default Ember.Component.extend({
url: null,
looping: false,
playing: true,
muted: true,
highlightedStyle: '',
click(event) {
this.get('onClickCallback') (this.get('videoPos'));
event.stopPropagation();
},
mouseEnter() {
this.get('onHoverCallback') (this.get('videoPos'));
},
willClearRender() {
this.set('playingObserver', null);
this.set('urlObserver', null);
},
playingObserver: Ember.observer('playing', function() {
if (this) {
var p = this.get("playing");
var videoElement = this.$().find("video").get(0);
if (videoElement) {
if (p) {
videoElement.play();
}
else {
videoElement.pause();
}
}
else {
console.log("No video element found!");
}
}
}),
urlObserver: Ember.observer('url', function() {
if (this) {
var videoElement = this.$().find("video").get(0);
if (videoElement) {
videoElement.load();
}
else {
console.log("No video element found");
}
}
}),
actions: {
ended() {
if (this.get('looping')) {
this.$().find("video").get(0).play();
console.log("video-player ended");
}
else {
console.log(this.get('videoPos'));
this.get('onEndedCallback') (this.get('videoPos'));
}
},
play() {
if (this.get('playing')) {
this.$().find("video").get(0).play();
}
}
}
});
I can post more code if it would help shed light on the culprit, thanks!
I found the culprit of the lag. The issue was in the parent container, content-area.js, which had a resetTimeout action that was being called incorrectly, which caused the focus to cycle needlessly, resulting in the lag.
Also implemented a switch off in terms of rendering videos to ensure smooth loading from one video to the next in video-stack.js, there are now 2 video objects, A & B, which are fetched and preloaded from the blob object, showing one while the other is hidden. Once the displayed video ends, they swap out, and the next video in the stack is loaded.
video-stack.js
export default Ember.Component.extend({
selectedVidAPos: 0,
selectedVidBPos: 0,
selectedStackIndex: 0,
stackStyle: '',
playerSize: '',
isMuted: true,
showVidA: true,
init() {
...
}
},
videoA: Ember.computed('videos', 'selectedVidAPos', function () {
return this.get('videos')[this.get('selectedVidAPos')];
}),
videoB: Ember.computed('videos', 'selectedVidBPos', function () {
return this.get('videos')[this.get('selectedVidBPos')];
}),
actions: {
stackClicked() {
this.get('onClickCallback') (this.get('videos'), (this.get('showVidA') ? this.get('selectedVidAPos') : this.get('selectedVidBPos')));
},
getNextVideoA() {
let arrayLength = this.get('videos').length;
if (arrayLength === 1) {
return;
}
let curArrayPos = parseInt(this.get('selectedVidAPos'));
this.set('selectedVidAPos', (curArrayPos + 2) % arrayLength);
this.set('showVidA', false);
},
getNextVideoB(){
let arrayLength = this.get('videos').length;
if (arrayLength === 1) {
return;
}
let curArrayPos = parseInt(this.get('selectedVidBPos'));
this.set('selectedVidBPos', (curArrayPos + 2) % arrayLength);
this.set('showVidA', true);
},
stackHovered() {
this.get('onHoverCallback') (this.get('videos'), this.get('selectedStackIndex'));
}
}
});
content-area.js
import Ember from 'ember';
import KeyboardControls from '../mixins/keyboard-controls';
export default Ember.Component.extend(KeyboardControls, {
displayVideoSelect: false,
displayVideoSelectTimeout: null,
displayVideo: false,
video: null,
videoPlaying: false,
keyboard: null,
backgroundVideoPos: 0,
backgroundVideoUrl: null,
backgroundVideoKeys: null,
selectionVideos: [],
stackListData: null,
showVideoSelect: function() {
this.set('displayVideoSelect', true);
this.send('resetTimeout');
},
hideVideoSelect: function() {
this.set('displayVideoSelect', false);
clearTimeout(this.get('displayVideoSelectTimeout'));
},
pauseVideo: function() {
this.set('videoPlaying', !this.get('videoPlaying'));
this.set('displayVideoSelect', !this.get('videoPlaying'));
this.set('focus', this.get('videoPlaying'));
},
select: function() {
this.set('videoPlaying', false);
this.set('focus', false);
this.showVideoSelect();
this.send('resetTimeout');
},
cancel: function() {
this.pauseVideo();
this.send('resetTimeout');
},
goNext: function() {
this.pauseVideo();
this.send('resetTimeout');
},
goPrevious: function() {
this.pauseVideo();
this.send('resetTimeout');
},
updateFocus: function(param) {
if (param) {
this.$().attr('tabindex', 2);
this.$().focus();
}//if
else {
this.$().attr('tabindex', -2);
this.$().blur();
}//else
},
init() {
...
},
click() {
this.set('focus', false);
this.showVideoSelect();
},
actions: {
videoSelected(sender, videoData) {
...
},
videoEnded() {
this.set('focus', false);
this.showVideoSelect();
this.set('displayVideo', false);
},
cycleBackground() {
...
},
cancelPressed() {
this.cancel();
},
resetTimeout() {
let component = this;
clearTimeout(this.get('displayVideoSelectTimeout'));
let timeout = setTimeout(() => {
component.hideVideoSelect();
//This set command was responsible for the lag
component.set('focus', true);
}, this.get('data.config.ui.idle') * 1000);
this.set('displayVideoSelectTimeout', timeout);
}
}
});

In Vue.js, is this a wrong writing for v-on?

i want to make two button to input number.
but when the left one goes to 10, it looks like this:
enter image description here
i want it to be 2 on the left while 0 on the right side.
so i changed my code:
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal2"></button-counter>
<button-counter v-on:increment2="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
template: '<button v-on:click="increment">{{ counter }}</button><button v-on:click="increment2">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
increment: function () {
this.counter += 1
this.$emit('increment')
},
increment2:function () {
if(this.counter === 10){
this.counter = 0;
this.increment();
}
this.$emit('increment2')
}
},
})
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
},
incrementTotal2: function () {
this.total = this.total +10
}
}
})
but it did'nt work..enter image description here
i click the right button, the total number wont change.
You render 2 components each of them should render 2 buttons. Sounds about right? If you check Element Inspector you will see that rendered only 2 buttons. 2 + 2 === 2 - something is fishy...
Dev version of Vue telling you in console "Error compiling template... Component template should contain exactly one root element".
So each button-counter render first button => writing you warning => and ignoring second button.
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter-1 #increment="incrementTotal"></button-counter-1>
<button-counter-2 #increment="incrementTotal"></button-counter-2>
</div>
Vue.component('button-counter-1', {
template: '<button #click="increment1">{{ counter }}</button>',
data: function() {
return { counter: 0 }
},
methods: {
increment1: function () {
this.counter++;
this.$emit('increment', 10);
}
}
});
Vue.component('button-counter-2', {
template: '<button #click="increment2">{{ counter }}</button>',
data: function() {
return { counter: 0 }
},
methods: {
increment2: function () {
this.counter++;
this.$emit('increment', 1);
}
}
})
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function (n) {
this.total += n;
},
}
})

Categories