Vue.js Computed/Watch Not Reacting to Viewport Width Data Change - javascript

Can someone explain to me in simple terms why the following example doesn't work?
I'm trying to run a function that captures the viewport/window width and then runs code based on how wide the viewport or window is (responsive design).
I'm a beginner so it's entirely possible I'm misunderstanding how Watch and Computed works... but it's my understanding that both Watch and Computed monitors a data property and if my data changes, watch and computed should react and trigger their code right?
So if I have a value called viewportWidth in my data, and I run an onresize to continually update it, I am updating my data which should trigger my watcher right? Shouldn't the continually updating value also trigger my computed property since it also relies on changing data?
So far I'm not seeing either of them react to my data changing.. if I'm misunderstanding please ELI5 and show me the better way to approach this and why..
(quick sidenote: I understand I can just run my handler inside of my onresize listener, but I assumed it would be smarter to instead setup a watcher or computed so that my method since they cache(?) and not trigger too often when it doesn't need to and only update conditions when it needs to.. is that right?)
Thank you!
<template>
<main>
<section>
<h2>viewport width: {{viewportWidth}}px</h2>
<h2>computed: {{rackClass}}</h2>
<h2>Does it work? {{doesItWork}}</h2>
</section>
</main>
</template>
<script>
export default {
data() {
return {
viewportWidth: window.innerWidth,
doesItWork: 'no it does not'
}
},
mounted() {
window.onresize = function(e) {
this.viewportWidth = window.innerWidth;
console.log(window.innerWidth)
}
},
watch: {
viewportWidth: function() {
console.log('>> value changed')
this.handleViewPortChange();
}
},
computed: {
rackClass: function(){
let theValue = "greater";
if(this.viewportWidth > 1000) theValue = "less than"
console.log('>> viewportWidth changed = ',this.viewportWidth)
return theValue
},
methods:{
handleViewportChange: function() {
this.doesItWork = 'it works!';
}
}
}
}
</script>
https://codepen.io/cmaxster/pen/rNyZLXG

Well aren't you in a pickle!
You are putting your curly braces and the commas in all the wrong places!
I updated the code so that it can be added as a snippet here. I have also put comments where you had messed up.
const app = new Vue({
el: "#app",
data() {
return {
viewportWidth: window.innerWidth,
doesItWork: 'no it does not'
}
},
mounted() {
const self = this;
window.onresize = (e) => {
this.viewportWidth = window.innerWidth;
//console.log(window.innerWidth)
}
},
watch: {
viewportWidth: function() {
console.log('>> value changed')
this.handleViewportChange(); // you were calling the wrong method! spellings and case was messed up
}
},
computed: {
rackClass: function(){
let theValue = "greater";
if(this.viewportWidth > 1000) theValue = "less than"
console.log('>> viewportWidth changed = ',this.viewportWidth)
return theValue
}
},
// you had your methods inside computed!
methods:{
handleViewportChange() {
this.doesItWork = 'it works!';
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12/dist/vue.js"></script>
<main id="app">
<section>
<h2>viewport width: {{viewportWidth}}px</h2>
<h2>computed: {{rackClass}}</h2>
<h2>Does it work? {{doesItWork}}</h2>
</section>
</main>

Have you tried transforming your watch into an arrow function?

Related

Vue acting weird with condition relating to window width

So I am just trying to get my basic Vue navbar functions working such as changing class on scroll (which works fine) and changing class on resize, which I'm having a bit more trouble with.
Here is the content of my <template> tags:
<nav class="navbar is-fixed-top navbar-max">
{{windowWidth}}
</nav>
...and the relevant content of my <script> tags:
export default {
name: "Navbar",
data () {
return {
windowWidth: window.innerWidth
}
},
created () {
window.addEventListener('resize', this.onResize);
},
mounted () {
this.windowWidth = window.innerWidth
},
beforeDestroy () {
window.removeEventListener('resize', this.onResize);
},
methods: {
onResize() {
let navbar = document.querySelector(".navbar");
if (this.windowWidth > 768) {
console.log(this.windowWidth),
navbar.classList.remove("nav-mobile"),
navbar.classList.add("nav-desktop")
}
else {
console.log(this.windowWidth),
navbar.classList.remove("nav-desktop"),
navbar.classList.add("nav-mobile")
}
}
}
}
My issue is really odd - all my console.log()'s output the correct width, and so does {{windowWidth}} in the navbar, and even the adding and removing classes works! It's just that the changed classes don't seem to have any effect until windowWidth = 1024 and I have no idea why...
Any help?
Cheers :)
You never set this.windowWidth after mount.
Simply add the following:
onResize() {
this.windowWidth = window.innerWidth // add this line
...
}
I would also like to point out that what it looks like you're trying to achieve (different nav-bar-styling on mobile and desktop) is very doable without any vue-/js-magic using CSS #media-queries.
And if you still wish to do it with vue, you should do it the vue way:
Make a computed method like so:
computed: {
isMobile() {
return this.windowWidth <= 768
}
}
and then update the class directly on the nav-tag using class-binding.

How to add displays without re-rendering the whole component in VueJS

I'm just new to Laravel and Vuejs. And I have this problem wherein the whole component is re-rendering when the "Load More" button is clicked or when scrolled down to the bottom. The button or scroll is just acting like a pagination, but all you can do is to load more or add more displays. My problem is how can i render the new displays without re-rendering the whole component.
I tried creating a variable wherein it will pass how many paginate will be displayed. Yes it does the work but the component is re-rendering and the size of the reply from the server gets larger and larger.
here's my script on my Vue component:
<script>
export default {
props: ['user','review_count'],
data(){
return{
reviews: {},
limit: 2,
scrolledToBottom: false,
}
},
created(){
this.getReviews();
this.scroll();
},
methods: {
getReviews: function(page){
axios.get('/datas/reviews?user='+ this.user + '&limit='+ this.limit)
.then((response) =>{
this.reviews = response.data.data;
})
.catch(()=>{
});
},
countType: function(data, type) {
return data.filter(function(value) { return value.type === type }).length;
},
loadMore: function(){
this.limit+=6;
this.getReviews();
},
scroll () {
window.onscroll = () => {
let bottomOfWindow = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight === document.documentElement.offsetHeight
if (bottomOfWindow&&this.review_count>this.limit) {
this.loadMore();
}
}
}
}
}
</script>
here's my controller:
public function reviews()
{
if($users = \Request::get('user')){
if($limit = \Request::get('limit')){
$reviews = Review::select(\DB::raw('id, product_id, review_rating, review_content'))
->where('user_id', $users)
->with('products:product_image,id,product_name')
->with('activities')
->orderBy('reviews.created_at', 'DESC')
->paginate($limit);}}
return $reviews;
}
I just solved my own Question, the solution is using pagination. I just pushed all of the data from the server in an object every time it scrolled down.
here's my code for scroll down:
scroll () {
window.onscroll = () => {
let bottomOfWindow = Math.max(window.pageYOffset, document.documentElement.scrollTop, document.body.scrollTop) + window.innerHeight === document.documentElement.offsetHeight
if (bottomOfWindow&&this.review_count>this.reviews.length) {
this.getReviews();
}
}
}
here's my codes for getReviews():
getReviews: function(){
let vm = this;
axios.get('/datas/reviews?user='+ this.user + '&page='+ this.page)
.then((response) =>{
$.each(response.data.data, function(key, value) {
vm.reviews.push(value);
console.log((vm.reviews));
});
})
.catch(()=>{
});
this.page+=1;
},
I've come up with this idea so that I will not use pagination anymore to view the next posts. It's more like a infinite scroll pagination component.
In my opinion, there is two things you need to do.
1/ in stead of increasing the limit eveytime, you should look into paging your results on the server, so you can ask the next page from your server. Now, on each consecutive call you are fetching what you already had again, which will eliminate your goal of making this less stressful on your server
2/ in your client code obviously you need to support the paging as well, but also make sure you properly set your key on the looped elements. VueJS used the key to determine whether it should rerender that particular element in the loop.
Let me know if this helps!

Vue Transition - JavaScript hooks

Based on this answer, I'm trying to create a Vue slideToggle component using transition.
The slideToggle is a classic paradigm in height animation. I've been successful so far...
I don't want to set a fixed max-height or height, I want the height to be dynamic.
My animation is working properly when displaying and hiding. The problem is in canceling while displaying or hiding.
How to handle the #enter-cancelled and the #leave-cancelled hooks? I'm new to vue transitions :)
I put my code inside this CodeSandBox: https://codesandbox.io/s/vue-template-3b7oj
I don't know if this helps you, but try this:
declare a new variable:
data() {
return {
height: null,
toggling: false
};
},
when the open or close function start, verify if toggling is true, if yes, just cancel, like this:
enter(el) {
if (this.toggling) return;
this.toggling = true;
this.height = el.offsetHeight;
el.style.overflow = "hidden";
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
el.style.marginTop = 0;
el.style.marginBottom = 0;
setTimeout(() => {
el.style.transitionProperty = `height, margin, padding`;
el.style.transitionDuration = this.duration + "ms";
el.style.height = this.height + "px";
el.style.removeProperty("padding-top");
el.style.removeProperty("padding-bottom");
el.style.removeProperty("margin-top");
el.style.removeProperty("margin-bottom");
this.toggling = false;
});
},
Will be something like this:
https://codesandbox.io/s/vue-template-78n7t?fontsize=14
Maybe i broke your code, sorry, but will you get the idea.
As per the offical documentation Javacript transition hooks
the #leave-cancelled is only available with v-show, where are in your sample code you are using v-if, if you change this you will be able to capture the #leave-cancelled hook,#leave-cancelled and #enter-cancelled are triggered when enter or leave are interrupted, say you press the toggle button while opening as well as pressing the button while its closing.
Vue-Transition-Cancel
tl;dr
leave event cancels not yet called enter
enter cancels not yet called leave
cancel state is stored in
el._enterCb.cancelled
el._leaveCb.cancelled
analysis
Consider:
const cb = el._enterCb = once(() => {
if (expectsCSS) {
removeTransitionClass(el, toClass)
removeTransitionClass(el, activeClass)
}
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, startClass)
}
enterCancelledHook && enterCancelledHook(el)
} else {
afterEnterHook && afterEnterHook(el)
}
el._enterCb = null
})
source: _enterCb
So a naive solution to cancel #enter is
el => {el._enterCb.cancelled = true; done()}
This is what actually happens when one triggers leave
// call enter callback now
if (isDef(el._enterCb)) {
el._enterCb.cancelled = true
el._enterCb()
}
source: leave
Same applies to:
const cb = el._leaveCb = once(() => {
if (el.parentNode && el.parentNode._pending) {
el.parentNode._pending[vnode.key] = null
}
if (expectsCSS) {
removeTransitionClass(el, leaveToClass)
removeTransitionClass(el, leaveActiveClass)
}
if (cb.cancelled) {
if (expectsCSS) {
removeTransitionClass(el, leaveClass)
}
leaveCancelled && leaveCancelled(el)
} else {
rm()
afterLeave && afterLeave(el)
}
el._leaveCb = null
})
source: _leaveCb
One can check for possible assignments:
https://github.com/vuejs/vue/search?q=_leaveCb&unscoped_q=_leaveCb

Set time out event on a m() generated element

I have a m("p.help") element which is removed with a click event.
I also want the element to be removed automatically after a few seconds if not clicked. I need to set a time out on it. Setting time out does not work.
function help() {
var text = `This is a service template. Use Service section to set the schedule`;
var node = m("p.help", {
onclick() {
this.parentNode.removeChild(this);
},
}, text);
setTimeout(() => {
if (node.parentNode) {
node.parentNode.removeChild(node);
console.log("removed");
m.redraw();
}
}, 5000);
return node;
}
The click event works fine but the time out does not work. It is not even triggered judging by the console.log()
What am I doing wrong?
EDIT
Thanks ciscoheat for the tip.
I had to put the timer in the controller for this to work.
So this one works fine:
function controller(init) {
this.display = {
help: true
};
setTimeout(() => {
this.display.help = false;
m.redraw();
}, 5000);
}
function view(vm) {
return m(".container", [
(() => {
var text = "Some text";
if (vm.display.help) {
return m("p.memo", {
onclick() {
this.parentNode.removeChild(this);
}
}, text);
}
})(),
]);
}
To use Mithril correctly, you should avoid DOM manipulation, leaving that to Mithril's fast diff algorithm.
Use a state variable instead, related to displaying the help paragraph that will be changed automatically after 5 seconds.
Here's a jsbin showing what I mean: http://jsbin.com/kixece/edit?html,js,output

Sinon Spy not working with Javascript call or apply

I'm trying to use sinon.spy to check if the play function is being called for a component. The problem is that the spy counter is not updating, even though I've confirmed that my component's function is indeed being called.
I've tracked it down to the use of Javascript's call function:
handleDone: function(e) {
for (var i = 0; i < this.components.length; i++) {
if (this.components[i].element === e.target) {
if (this.components[i].hasOwnProperty("play")) {
// This won't trigger the spy.
this.components[i]["play"].call(this.components[i].element);
}
break;
}
}
}
A similar thing happens when swapping call for apply.
[UPDATED]
Here is the relevant test:
var page = document.getElementById("page"),
demo = document.getElementById("demo"),
playSpy;
suiteSetup(function() {
playSpy = sinon.spy(demo, "play");
});
suiteTeardown(function() {
demo.play.restore();
});
suite("done", function() {
test("rise-component-done fired from element with play property", function() {
assert(playSpy.notCalled); //true
demo.sendDone();
assert(playSpy.calledOnce); //false
});
});
And the play and sendDone functions in the demo component:
play: function() {
console.log("play");
},
sendDone: function() {
this.dispatchEvent(new CustomEvent("rise-component-done", { "bubbles": true }));
}
The page component is registered to listen for this event:
this.addEventListener("rise-component-done", this.handleDone);
Anyone know of a workaround?
Thx.
So the problem wasn't actually related to the call method at all as Ben illustrated. After much tinkering, I realized I had to change this line:
playSpy = sinon.spy(demo, "play");
to this:
playSpy = sinon.spy(page.components[0], "play");
Now my tests pass. Hooray! I just wish it hadn't taken me the better part of a day to debug.

Categories

Resources