Vue3, vue-router, how to keep unused view mounted, with audio playback (more than keep-alive)? - vue-router

I want to use vue-router, but then keep a currently unused view mounted in the DOM. Something like Vue's keep-alive but with just hiding the unused view, instead of unmounting it.
The reason is, this view has an audio player in it, and I want to keep it playing. On unmount, it's stopping. Here's how it looks currently:
<!-- To keep the audio within the media player component running,
simply keep all components alive over route changes -->
<router-view v-slot="{ Component }">
<keep-alive include="Play">
<component :is="Component" />
</keep-alive>
</router-view>
I would like to replace the keep-alive with something like keep-mounted (but hidden). How can I do that?
Notes:
Of course I already have tried using Vue's keep-alive, as suggested here: https://stackoverflow.com/a/66297873/79485, and it's working as advertised. However, It's not enough, I need to keep the component mounted, not just kept in memory.
I have found https://github.com/Akryum/vue-router-multi-view, but this is vue2 only.

I have found a solution for the specific problem at hand, being the interrupted audio playback. As suggested by #Thomas I have placed the audio element completely out of the DOM, using the Web Audio API.
Here's how I create the audio context with the element, right at component creation:
data: () => ({
//...
/** The audio context to use
audioContext: new AudioContext({
latencyHint: 'interactive',
}),
audioElement: document.createElement('audio'),
}),
I never add the audio element to the DOM, but use it with the received element handle as usual:
/** Handles the setup of the audio graph outside the mounted lifespan.
* #devdoc The audio element is intentionally not added to the DOM, to keep it unaffected of unmounts during vue-router route changes.
*/
created() {
this.audioElement.src = this.src;
//.....
},
With this, although the VueJs3 view gets lifted from the DOM with keep-alive, the audio is unaffected of this.
I then subsequently destroy the audio context and the audio element at the unmounted() lifecycle event.
This works very well. You may review the complete code at https://github.com/suterma/replayer-pwa/blob/a5acd8dc3b5a277bca6d9baf9459fb70eea2e1a5/src/components/TrackAudioApiPlayer.vue

Related

Use Vimeo Player SDK with Nuxt.js

I want to use the library like Vimeo Player SDK with Nuxt.js.
<template>
<iframe
id="video01"
ref="video01"
src="https://player.vimeo.com/video/{videoID}">
</iframe>
</template>
<script>
import Player from '#vimeo/player'
export default {
created () {
const iframe = this.$refs.video01
const player = new Player(iframe)
}
}
</script>
But I cannot get DOM.
In case of SPA, I couldn't get DOM.
So error log is here.
TypeError: You must pass either a valid element or a valid id.
In case of SSR, window cannot use.
So error log is here.
window is not defined
Can't I use Vimeo Player SDK with Nuxt.js?
Did you try the vue-vimeo-player package? It could be helpful to have something managed the Vue way.
For the SPA, you will need to wait for the element to be on the page; hence try your code in a mounted hook (created step is before anything get mounted to the DOM).
If you want SSR too, you could try to wrap the whole thing into a
<client-only>
<! --- note I used vue-vimeo-player and not the vimeo-player --->
<vue-vimeo-player ref="player" :video-id="yourVideoID"/>
</client-only>
Here is a repo that I've created to show you how simple it is to make it work: https://github.com/kissu/so-nuxt-vimeo/blob/master/pages/index.vue
It made me find that you should use the <vue-vimeo-player /> component and not <vimeo-player />.
However, I imported the Vue wrapper for Vimeo Embed Player in the demo locally in a Vue component.

Nuxt: How to know that hydration has been finished?

Situation
I save tour pages that user has visited in order to show "Recently visited tours" on the home page when user comes back to the site. Visited tours I save with vuex-persistedstate.
The code is simple: <TheVisitedTours v-show=toursRecentlyViewed.length" />
Problem
When user comes back, there are the next hydration issues:
Parent: <div style=​"display:​none;​" data-v-24f9a6f4>​…​</div>​
Mismatching childNodes vs. VNodes: NodeList(3) [comment, text, comment] (5) [VNode, VNode, VNode, VNode, VNode]
[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.
It is obvious that SSR does not have info about Visited tours and vuex-persistedstate restores info about visited tours before hydration is finished.
In case if I use v-show instead of v-if I see:
[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.
I have tried to wrap it into <client-only>...</client-only> but it does not help.
Also I have tried to use:
mounted () {
this.isMounted = true
}
and
mounted () {
window.onNuxtReady(() => {
this.isMounted = true
})
}
with
<TheVisitedTours v-if=isMounted && toursRecentlyViewed.length" />
but it also does not work.
Question
Is there any event I can listen for to know that hydration has been finished? If no, maybe someone have any workaround ideas?
Thanks a lot.
Found solution.
My initial code used vue-lazy-hydration
<LazyHydrate when-visible>
<b-container>
......
<client-only>
<TheVisitedTours v-show=toursRecentlyViewed.length" />
</client-only>
</b-container>
</LazyHydrate>
Once LazyHydrate removed, <client-only> fixed an issue.

Vue rendering only part of my App.vue after a login

I am working on a Vue.js application that I am almost done, one major bug left. The bug/issue is that when you go to /login and login to the site you get redirected via a router push (tried replace too) and when this happens I want to render the whole dashboard. Currently since in my App.vue file the router view is a different part it only renders the dashboard info part and not my header or sidebar.
Pretty much imagine a dashboard without a header or sidebar. That's what's rendering. I'd be okay if I could do something like F5 does because then it all would load correctly though taking up to 2 seconds longer on login which is okay by me.
My App.vue file template code
<template>
<div class="fade page-sidebar-fixed page-header-fixed show page-container" v-if="!pageOptions.pageEmpty" v-bind:class="{
'page-sidebar-minified': pageOptions.pageSidebarMinified,
'page-content-full-height': pageOptions.pageContentFullHeight,
'page-with-top-menu': pageOptions.pageWithTopMenu,
'page-sidebar-toggled': pageOptions.pageMobileSidebarToggled,
'has-scroll': pageOptions.pageBodyScrollTop
}">
<Header />
<Sidebar v-if="!pageOptions.pageWithoutSidebar" />
<div id="content" class="content" v-bind:class="{ 'content-full-width': pageOptions.pageContentFullWidth, 'content-inverse-mode': pageOptions.pageContentInverseMode }">
<router-view></router-view>
</div>
</div>
<div v-else>
<router-view></router-view>
</div>
</template>
Looks like I have resolved my issue, it comes from vue-router and how I am doing that if statement in my template code. So in that code I am checking a boolean value then choosing which view to render. So I had though on all of my auth pages I set the value correctly on exit. Turns out not...
This was in my Login.vue file, idea was to have on an exit of the route that it would change the boolean to false which would let me render it right. This was something I did initally but had forgotten about till about 20 minutes ago.
Upon checking this I found the value was not being changed for some reason. So as a work around in the created part of my Dashboard.vue file I set the value to false explicitly
Login.vue
beforeRouteLeave (to, from, next) {
PageOptions.pageEmpty = false;
next();
},
Dashboard.vue
created() {
PageOptions.pageEmpty = false;
...
}
The main idea is to have several base pages each one of them is operate with its own set of internal views.
You have to redirect user to another view, which is the one and only active view and this view can contains sidebar header and main part that also contains router-view, and then! you load any needed components in it.
You have to have something like that:
App component is only contains router view tag and any other pages are load into this.
The routes structure then looks like that:
As you can see, there are two base views load in App view. And then the base view can has a lot of children. The level of nested routes is up to you. Here is the contents of my app Home view:
And the MainContent component which is contains router view only:
The good example of project structure is the one generated with vue-cli. You can use it to simplify dev process with a lot of benefits and good practice solutions.

Vue.js showing content that should be hidden by a v-if tag when user refreshes page

I have a Vue.js app that loads content in the created() method. I use a v-if tag to hide all of my UI until that content is loaded and ready to go. It works fine on the initial load, but if a user were to hit refresh in Chrome then the app displays (flashes momentarily) content that would not otherwise be displayed (based on the data being loaded in created).
From what I understand using the v-if tag, with a flag from my vuex store that indicates when the load has completed, is the proper way to hide content until I am ready to display it.
How can I avoid having the content flash on the refresh?
Vue.JS has solved this using the v-cloak directive. (see docs)
You add the v-cloak directive to your application's root element:
<div id="app" v-cloak>
...
</div>
Then add this CSS rule to your application's stylesheet:
[v-cloak] {
display: none;
}
Everything within the app div will then be hidden until Vue has initialized.

Vue prerender flickering

I have the following solution now:
<template>
<section id="prod-main">
<prod-preview v-for="prod in products" :id="prod.id" :key="prod.id"/>
</section>
</template>
export default {
...
computed: {
products: function () {
return this.$store.getters['products/getPreview']
}
}
...
}
Vuex store will receive info after some delay from my backend. So at first call it will be empty. Now I want to use vue spa prerender and here I see a flickering.
As I understood it works like:
1. Browser loads HTML with products
2. Execute js that replace products with nothing because the store is empty.
3. After some delay shows it again with backend info.
How can I fix it? I should left prerender for indexing and I can't hardcode the backend reply.
You can use the setting captureAfterTime to wait for your async call to complete, before saving the html of the page.
Other settings are available :
// NOTE: Unless you are relying on asynchronously rendered content,
// such as after an Ajax request, none of these options should be
// necessary. All synchronous scripts are already executed before
// capturing the page content.
// Wait until a specific event is fired on the document.
captureAfterDocumentEvent: 'custom-post-render-event',
// This is how you would trigger this example event:
// document.dispatchEvent(new Event('custom-post-render-event'))
// Wait until a specific element is detected with
// document.querySelector.
captureAfterElementExists: '#content',
// Wait until a number of milliseconds has passed after scripts
// have been executed. It's important to note that this may
// produce unreliable results when relying on network
// communication or other operations with highly variable timing.
captureAfterTime: 5000,
Another issue can be related to how the prerendered HTMl gets hydrated, i've openned an issue on github, but they still haven't addressed it (and are not willing to ?)
https://github.com/chrisvfritz/prerender-spa-plugin/issues/131
The solution is to add data-server-rendered="true" to your vuejs parent node in the prerendered html, like this:
<div id="root" data-server-rendered="true">...
You can use the option postProcessHtml to do so.
I don't know if I understand your problem here but have you tried to add a v-if to avoid flickering:
<template>
<section id="prod-main">
<prod-preview
v-if="products.length > 0"
v-for="prod in products"
:id="prod.id"
:key="prod.id"/>
</section>
</template>