Does the "before" hook trigger a render when leveraging transition hooks in a render function? - vue.js

I am trying to create a movement transition within a render function using appear hooks. I thought that putting initial state of the transition within the beforeAppear hook and then setting the finalized state in the appear hook would cause an element to be enough for a transition to occur.
However, what seems to be happening is that the style that I set in the beforeAppear either never renders, or is overwritten in the appear hook before the browser knows that it should transition.
Below is an example of what I am trying to accomplish. Using Vue.nextTick() does not give me what I was hoping. However, I do see that using a setTimeout() will give me what I am effectively looking for, but I thought that this was the built-in functionality of the appear hook considering the docs Transition Classes:
v-enter: Starting state for enter. Added before element is inserted, removed one frame after element is inserted.
v-enter-active: Active state for enter. Applied during the entire entering phase. Added before element is inserted, removed when
transition/animation finishes. This class can be used to define the
duration, delay and easing curve for the entering transition.
I understand that these are for the classes that are applied to the element, but don't these map 1:1 with the hooks?
const app = new Vue({
el: "#app",
render(h) {
// Just hooks
const transitionTestJustHooks = h("transition", {
props: {
css: false,
appear: ""
},
on: {
beforeAppear(el) {
console.log("before-appear");
el.style.position = "absolute";
el.style.transition = "left 2s ease";
el.style.top = "0em";
el.style.left = "20px";
},
appear(el, done) {
console.log("appear");
el.style.left = "200px";
done();
}
}
}, [h("div", "just-hooks")]);
// Vue.nextTick()
const transitionTestNextTick = h("transition", {
props: {
css: false,
appear: ""
},
on: {
beforeAppear(el) {
console.log("before-appear");
el.style.position = "absolute";
el.style.transition = "left 2s ease";
el.style.top = "1em";
el.style.left = "20px";
},
appear(el, done) {
console.log("appear");
Vue.nextTick(() => {
el.style.left = "200px";
done();
});
}
}
}, [h("div", "Vue.nextTick()")]);
// setTimeout()
const transitionTestSetTimeout = h("transition", {
props: {
css: false,
appear: ""
},
on: {
beforeAppear(el) {
console.log("before-appear");
el.style.position = "absolute";
el.style.transition = "left 2s ease";
el.style.top = "2em";
el.style.left = "20px";
},
appear(el, done) {
console.log("appear");
setTimeout(() => {
el.style.left = "200px";
done();
});
}
}
}, [h("div", "setTimeout")]);
return h("div", [
transitionTestJustHooks,
transitionTestNextTick,
transitionTestSetTimeout
]);
}
});
<script src="https://unpkg.com/vue#2.6.10/dist/vue.min.js"></script>
<div id="app"></div>

Related

VueJs Quasar - scroll position stuck using Quasar loading component after page change

I have a problem using Quasar with VueJs.
When I use Quasar's Loading.show() method, the scroll is stuck when I go to the new page, and the top of the new page is not shown.
In the router/index.js file i have set
const router = new VueRouter({
scrollBehavior: () => ({ x: 0, y: 0}),
...
});
For example:
In MainLayout.vue (which is the parent component of the index and category page), I have set a watch to show the loading screen when the isLoading flag is true in the state.
watch: {
isLoading: {
deep: true,
handler(isLoading) {
if(isLoading == true) {
Loading.show();
} else {
Loading.hide();
}
}
}
}
This is working fine, but when I have clicked a link to a category page after I have scrolled a little bit, The category page will also retain the previous scroll position.
Is working without the scroll issue with commented out Loading.show();
Video from the issue:
https://drive.google.com/file/d/1d-WaxHazNdZ8bp3_pcN14hcayucUYYUb/view
Try to do this, but I'm not sure if this is the right solution:
import { scroll } from 'quasar'
const { getScrollTarget, setVerticalScrollPosition } = scroll
watch: {
isLoading: {
deep: true,
handler(isLoading) {
if(isLoading == true) {
Loading.show();
} else {
Loading.hide();
const el = document.querySelector('.q-page-container')
const target = getScrollTarget(el)
const offset = el.offsetTop
const duration = 0
setTimeout(() => setVerticalScrollPosition(target, offset, duration), 400);
}
}
}
}
Documentation https://quasar.dev/quasar-utils/scrolling-utils#scrolling-to-an-element

how to implement reusable api-calling component in vuejs?

I'm developing a simple vuejs app where I have a few identical APIs serving content that is parsed in a similar way. I would like to make the code to fetch the content common across the various API calls, and only have a need to pass the API endpoint to what fetches the content.
Here's my code
var content = new Vue({
el: '#story',
data: {
loaded: [],
current: 0,
hasMore:"",
nextItems:"",
errors: []
},
mounted() {
axios.get("/storyjs")
.then(response => {
this.loaded = this.loaded.concat(response.data.content)
this.hasMore = response.data.hasMore
this.nextItems = response.data.nextItem
}).catch(e => {
this.errors.push(e)
})
},
methods: {
fetchNext: function() {
axios.get(this.nextItems)
.then(response => {
this.loaded = this.loaded.concat(response.data.content)
this.hasMore = response.data.hasMore
this.nextItems = response.data.nextItem
this.current+=1
}).catch(e => {
//TODO CLEAR errors before pushing
this.errors.push(e)
})
},
next: function() {
if (this.current+1 < this.loaded.length) {
this.current+=1
} else {
this.fetchNext()
}
},
prev: function() {
this.current = (this.current-1 >= 0) ? this.current-1 : 0
}
},
delimiters: ['[{', '}]']
})
Right now, I've replicated the above object for stories, poems, and many other things. But I would ideally like to combine them into one. Strategies I tried to search for included having a parent component as this object, but I think I'm probably thinking wrong about some of this.
Really appreciate the help!
I went with mixins. This is the solution I implemented.
apiObject.js (Reusable object)
var apiObject = {
data: function() {
return {
loaded: [],
current: 0,
hasMore: "",
nextItems: "",
errors: []
};
},
methods: {
fetchContent: function(apiEndpoint) {
axios
.get(apiEndpoint)
.then(response => {
this.loaded = this.loaded.concat(response.data.content);
this.hasMore = response.data.hasMore;
this.nextItems = response.data.nextItem;
})
.catch(e => {
this.errors.push(e);
});
},
fetchNext: function() {
axios
.get(this.nextItems)
.then(response => {
this.loaded = this.loaded.concat(response.data.content);
this.hasMore = response.data.hasMore;
this.nextItems = response.data.nextItem;
this.current += 1;
})
.catch(e => {
//TODO CLEAR errors before pushing
this.errors.push(e);
});
},
next: function() {
if (this.current + 1 < this.loaded.length) {
this.current += 1;
} else if (this.hasMore == true) {
this.fetchNext();
}
},
prev: function() {
this.current = this.current - 1 >= 0 ? this.current - 1 : 0;
}
}
};
story.js (Specific usage)
var storyComponent = Vue.extend({
mixins: [apiObject],
created() {
this.fetchContent("/story");
}
});
new Vue({
el: "#story",
components: {
"story-component": storyComponent
},
delimiters: ["[{", "}]"]
});
and then, you could either define the template in the component itself, or use the inline-template way of creating the template in the html file, which is what I did
output.html with all js files included
<div id="story">
<story-component inline-template>
[{loaded[current].title}]
</story-component>
</div>
There are many ways to tackle this, but perhaps once you reach this level of complexity in the model of the components/application state, the most sensible strategy would be to use a central state store.
See the State Management chapter of the vue guide and possibly the excellent vuex.
There you could factor the common logic in suitable local classes/functions and call them from store actions (for async operations you have to use actions, which will commit mutations with respective state changes at completion of the asynchronous operations.

Vuejs 'beforeunload' event not triggered as expected

I have registered 'beforeunload' event on created hook of the component used by routes of vue router.
I want to call this event handler in order to remove user on browser tab close or browser tab refresh or browser close.
On ComponentA
created (){
window.addEventListener('beforeunload', () => {
this.removeUser()
return null
})
}
Smilarly on ComponentB
created (){
window.addEventListener('beforeunload', () => {
this.removeUser()
return null
})
}
And my router.js
{
path: '/staff/call/:session_key',
name: 'Staff Call',
component: ComponentA,
meta: {auth: true}
},
{
path: '/consumer/call/:session_key',
name: 'Consumer Call',
component: ComponentB
},
Here 'beforeunload' event handler is triggered randomly. That is sometimes it get triggered and sometimes not. I count find any pattern when it is triggered and when it is not.
What am I missing here?
Edit
I'd guess the most likely culprit then is exactly what #PatrickSteele said. From MDN:
Note: To combat unwanted pop-ups, some browsers don't display prompts
created in beforeunload event handlers unless the page has been
interacted with; some don't display them at all. For a list of
specific browsers, see the Browser_compatibility section.
I'd say it's likely you're seeing inconsistent behavior because you are sometimes not interacting with the page.
This may be a syntax error. created should be a method
created () {
window.addEventListener('beforeunload', this.removeUser)
},
methods: {
removeUser () {
//remove user here
}
}
A fiddle working: https://jsfiddle.net/e6m6t4kd/3/
It's work for me. while do something before reload or close in
vue.js
created() {
window.onbeforeunload = function(){
return "handle your events or msgs here";
}
}
I had to do some fiddling on the above examples, I believe this is the most robust solution:
let app1 = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
dirty_form: true,
},
created () {
console.log('created')
window.addEventListener('beforeunload', this.confirm_leaving)
},
methods: {
confirm_leaving (evt) {
if (this.dirty_form) {
const unsaved_changes_warning = "You have unsaved changes. Are you sure you wish to leave?";
evt.returnValue = unsaved_changes_warning;
return unsaved_changes_warning;
};
};
},
});
If you want detect page refresh/change in Vue whenever you press F5 or Ctrl + R, You may need to use Navigation Timing API.
The PerformanceNavigation.type, will tell you how the page was accessed.
created() {
// does the browser support the Navigation Timing API?
if (window.performance) {
console.info("window.performance is supported");
}
// do something based on the navigation type...
if(performance.navigation.type === 1) {
console.info("TYPE_RELOAD");
this.removeUser();
}
}
Not sure why none of the above were fully working for me in vue 3 composition api. Abdullah's answer partially works but he left out how to remove the listener.
setup() {
const doSomething = (e) => {
// do stuff here
return true
}
onBeforeMount(() => {
window.onbeforeunload = handleLeaveWithoutSaving
})
onUnmounted(() => {
window.onbeforeunload = null
})
}

vuejs2: how can i destroy a watcher?

How can i destroy this watcher? I need it only one time in my child component, when my async data has loaded from the parent component.
export default {
...
watch: {
data: function(){
this.sortBy();
},
},
...
}
gregor ;)
If you construct a watcher dynamically by calling vm.$watch function, it returns a function that may be called at a later point in time to disable (remove) that particular watcher.
Don't put the watcher statically in the component, as in your code, but do something like:
created() {
var unwatch = this.$watch(....)
// now the watcher is watching and you can disable it
// by calling unwatch() somewhere else;
// you can store the unwatch function to a variable in the data
// or whatever suits you best
}
More thorough explanation may be found from here: https://codingexplained.com/coding/front-end/vue-js/adding-removing-watchers-dynamically
Here is an example:
<script>
export default {
data() {
return {
employee: {
teams: []
},
employeeTeamsWatcher: null,
};
},
created() {
this.employeeTeamsWatcher = this.$watch('employee.teams', (newVal, oldVal) => {
this.setActiveTeamTabName();
});
},
methods: {
setActiveTeamTabName() {
if (this.employee.teams.length) {
// once you got your desired condition satisfied then unwatch by calling:
this.employeeTeamsWatcher();
}
},
},
};
</script>
If you are using vue2 using the composition-api plugin or vue3, you can use WatchStopHandle which is returned by watch e.g.:
const x = ref(0);
setInterval(() => {
x.value++;
}, 1000);
const unwatch = watch(
() => x.value,
() => {
console.log(x.value);
x.value++;
// stop watch:
if (x.value > 3) unwatch();
}
);
For this kind of stuff, you can investigate the type declaration of the API, which is very helpful, just hover the mouse on it, and it will show you a hint about what you can do:

Show loading spinner for async Vue 2 components

I have a fairly heavy component which I would like to load asynchronously, while at the same time showing the user a loading spinner when it's loading.
This is my first attempt, using loading defined in data linked to a spinner component with v-if="loading". Unfortunately this doesn't work because it seems that Vue doesn't rebind this properly for functions inside components -
export default {
data: {
return {
loading: false,
};
},
components: {
// ...
ExampleComponent: (resolve) => {
// Doesn't work - 'this' is undefined here
this.loading = true;
require(['./ExampleComponent'], (component) => {
this.loading = false;
resolve(component);
});
},
},
};
I've also found some Vue 1.0 examples, but they depended on $refs - in 2.0 $refs is no longer reactive, and cannot be used for this. The only way left is for the child component itself to do something on its mount lifecycle event to the application data state to remove the loading spinner, but that seems a bit heavy. Is there a better way to do this?
You could declare a variable outside the object scope (but still is within module scope) then use the created hook to attach this. So your updated code would look like:
let vm = {}
export default {
// add this hook
created () {
vm = this;
},
data: {
return {
loading: false,
};
},
components: {
// ...
ExampleComponent: (resolve) => {
// since 'this' doesn't work, we reference outside 'vm'
vm.loading = true;
require(['./ExampleComponent'], (component) => {
vm.loading = false;
resolve(component);
});
},
},
};