How to dynamically switch a component based on the existence of window in Nuxt.js? - vue.js

I have a dynamic component that looks different at different screen resolutions.
<template>
<div>
<headerComponent></headerComponent>
<div v-if="!large" class="placeholder"></div>
<component
v-else
:is="tariffBlock"
>
</component>
</div>
</template>
<script>
import smallComponent from '#/components/small-component'
import largeComponent from '#/components/large-component'
import headerComponent from '#/components/header-component'
const components = {
smallComponent,
largeComponent
}
export default {
components: {
headerComponent
},
data () {
return {
large: false
}
},
computed: {
getComponent () {
if (!this.large) return components.smallComponent
return components.largeComponent
}
},
created () {
if (process.browser) {
this.large = window.matchMedia('(min-width: 1200px)').matches
}
}
}
</script>
By default, a smallComponent is shown, and then a largeComponent. To avoid "jumping" I decided to show the placeholder while large === false.
To avoid the error window in not defined I use the check for process.browser.
PROBLEM: placeholder is only shown in dev mode, but when I start generate the placeholder is not displayed.
The following solutions DIDN'T help:
1.
created () {
this.$nextTick(() => {
if (process.browser) {
this.large = window.matchMedia('(min-width: 1200px)').matches
}
})
}
created () {
this.$nextTick(() => {
this.large = window.matchMedia('(min-width: 1200px)').matches
})
}
mounted () {
this.large = window.matchMedia('(min-width: 1200px)').matches
}
and with the addition process.browser and nextTick()
Creating a mixin with ssr: false, mode: client
Thanks in advance!

This is how you toggle between components in Nuxt.js
<template>
<div>
<div #click="toggleComponents">toggle components</div>
<hr />
<first-component></first-component>
<second-component></second-component>
<hr />
<component :is="firstOrSecond"></component>
</div>
</template>
<script>
export default {
data() {
return {
firstOrSecond: 'first-component',
}
},
methods: {
toggleComponents() {
if (this.firstOrSecond === 'first-component') {
this.firstOrSecond = 'second-component'
} else {
this.firstOrSecond = 'first-component'
}
},
},
}
</script>
You don't need to import them, it's done automatically if you have the right configuration, as explained here: https://nuxtjs.org/blog/improve-your-developer-experience-with-nuxt-components
In this snippet of code, first-component and second-component are shown initially (between the two hr) just to be sure that you have them properly loaded already. You can of course remove them afterwards.
Not recommended
This is what you're looking for. Again, this is probably not how you should handle some visual changes. Prefer CSS for this use-case.
<template>
<div>
<component :is="firstOrSecond"></component>
</div>
</template>
<script>
export default {
data() {
return {
firstOrSecond: 'first-component',
}
},
mounted() {
window.addEventListener('resize', this.toggleComponentDependingOfWindowWidth)
},
beforeDestroy() {
// important, otherwise you'll have the eventListener all over your SPA
window.removeEventListener('resize', this.toggleComponentDependingOfWindowWidth)
},
methods: {
toggleComponentDependingOfWindowWidth() {
console.log('current size of the window', window.innerWidth)
if (window.innerWidth > 1200) {
this.firstOrSecond = 'second-component'
} else {
this.firstOrSecond = 'first-component'
}
},
},
}
</script>
PS: if you really wish to use this solution, at least use a throttle because the window event will trigger a lot and it can cause your UI to be super sluggish pretty quickly.

Related

vue-i18n: language dependent view

I use vue-i18n in my application.
Now I would like to add an "About" view with a lot text and links.
I think it would be better maintainable to have several language dependent views than adding several {{ $t(...)}} in one view about.vue.
I thought about something like adding language ISO code to the view name:
.../about.en.vue
.../about.de.vue
.../about.es.vue
What would be the best way to combine and integrate with vue-i18n? Probably there is a different way?
You can use a dynamic component to achieve this:
<template>
<component :is="localizedAbout" />
</template>
<script>
import AboutEn from '../about.en.vue';
import AboutEs from '../about.es.vue';
import AboutDe from '../about.de.vue';
export default {
components: {
AboutEn,
AboutEs,
AboutDe,
},
computed: {
localizedAbout() {
switch (this.$i18n.locale) {
case 'en':
return AboutEn;
case 'es':
return AboutEs;
case 'de':
return AboutDe;
default:
return '';
}
},
},
}
</script>
After doing some other stuff, I was today able to solve this issue by using dynamic imports:
<template>
<b-container class="about">
<component :is="langComponent" />
</b-container>
</template>
<script>
export default {
name: 'AboutView',
data () {
return {
langComponent: null
}
},
mounted () {
this.$watch(
'$i18n.locale',
(newLocale, oldLocale) => {
if (newLocale === oldLocale) {
return
}
this.setLangAbout()
},
{
immediate: true
}
)
},
methods: {
setLangAbout () {
try {
import('#/components/about/About.' + this.$i18n.locale + '.vue').then(module => {
this.langComponent = module.default
})
} catch (err) {
console.log(err)
import('#/components/about/About.en.vue').then(module => {
this.langComponent = module.default
})
}
}
}
}
</script>
Thanks #Pochwar for your initial answer. Based on this I have done some more researched.
Following links helped me to solve this problem:
How does Dynamic Import in webpack works when used with an expression?
Comment at Error: Cannot find module with dynamic import

VueJS 2.x Child-Component doesn't react to changed parent-property

I have the problem, that a component doesn't recognize the change of a property.
The component is nested about 5 levels deep. Every component above the faulty one does update with the same mechanics and flawlessly.
I invested some time to get to the problem, but I can't find it.
The flow is:
Dashboard (change value and pass as prop)
TicketPreview (Usage and
pass prop)
CommentSection (Pass prop)
CommentList (FAULTY / Usage of prop)
Everything down to the commentSection is being updated as expected, but the commentList doesn't get the update notification (beforeUpdate doesn't get triggered).
Since I tested quite a few things I will only post the essential code from commentSection (parent) and commenList (child)
DISCLAIMER: This is a prototype code without backend, therefore typical API-Requests are solved with the localStorage of the users browser.
commentSection
<template>
<div id="comment-section">
<p>{{selectedTicket.title}}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList,
},
props: {
selectedTicket: Object,
},
beforeUpdate() {
console.log("Comment Section");
console.log(this.selectedTicket);
},
updated() {
console.log("Comment Section is updated");
}
}
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem,
},
data() {
return {
comments: Array,
}
},
props: {
selectedTicket: Object,
},
methods: {
getComments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for(let i = 0; i < comments.length; i++){
if (comments[i].ticketId === this.selectedTicket.id){
filteredComments.push(comments[i]);
}
}
this.comments = filteredComments;
}
},
beforeUpdate() {
console.log("CommentList");
console.log(this.selectedTicket);
this.getComments();
},
mounted() {
this.$root.$on("updateComments", () => {
this.getComments();
});
console.log("CL Mounted");
},
}
</script>
The beforeUpdate() and updated() hooks from the commentList component are not being fired.
I guess I could work around it with an event passing the data, but for the sake of understanding, let's pretend it's not a viable option right now.
It would be better to use a watcher, this will be more simple.
Instead of method to set comments by filtering you can use computed property which is reactive and no need to watch for props updates.
CommentSection
<template>
<div id="comment-section">
<p>{{ selectedTicket.title }}</p>
<comment-form :selectedTicket="selectedTicket" />
<comment-list :selectedTicket="selectedTicket" />
</div>
</template>
<script>
import CommentForm from "#/components/comment-section/CommentForm";
import CommentList from "#/components/comment-section/CommentList";
export default {
name: "CommentSection",
components: {
CommentForm,
CommentList
},
props: {
selectedTicket: Object
},
methods: {
updateTicket() {
console.log("Comment section is updated");
console.log(this.selectedTicket);
}
},
watch: {
selectedTicket: {
immediate: true,
handler: "updateTicket"
}
}
};
</script>
CommentList
<template>
<div id="comment-list">
<comment-item
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
</div>
</template>
<script>
import CommentItem from "#/components/comment-section/CommentItem";
export default {
name: "CommentList",
components: {
CommentItem
},
props: {
selectedTicket: Object
},
computed: {
comments() {
let comments = JSON.parse(window.localStorage.getItem("comments"));
let filteredComments = [];
for (let comment of comments) {
if (comment.ticketId == this.selectedTicket.id) {
filteredComments.push(comment);
}
}
// // using es6 Array.filter()
// let filteredComments = comments.filter(
// (comment) => comment.ticketId == this.selectedTicket.id
// );
return filteredComments;
}
}
};
</script>
I found the problem: Since commentList is only a wrapper that doesn't use any of the values from the prop, the hooks for beforeUpdate and updated are never triggered. The Vue Instance Chart is misleading in that regard. The diagram shows it like beforeUpdate would ALWAYS fire, when the data changed (then re-render, then updated), but beforeUpdate only fires if the Component and Parent has to be re-rendered.
The Object updates as expected, it just never triggered a re-render on the child component because the wrapper has not been re-rendered.

How can I use this component in a single component in Vue.js?

I am beginner of Vue.js.
I am trying to use a plugin called vue-base64-file-upload in a single component in VueCLI. However, it doesn't work in index.vue...
I have just changed the example (https://github.com/dhhb/vue-base64-file-upload) to this.
<template>
<div class="container">
<h2>Upload file</h2>
<vue-base64-file-upload
class="v1"
accept="image/png,image/jpeg"
image-class="v1-image"
input-class="v1-input"
:max-size="customImageMaxSize"
#size-exceeded="onSizeExceeded"
#file="onFile"
#load="onLoad" />
</div>
</template>
<script>
import VueBase64FileUpload from 'vue-base64-file-upload';
export default {
data () {
return {
customImageMaxSize: 3 // megabytes
};
},
components: {
VueBase64FileUpload
},
methods: {
onFile(file) {
console.log(file); // file object
},
onLoad(dataUri) {
console.log(dataUri); // data-uri string
},
onSizeExceeded(size) {
alert(`Image ${size}Mb size exceeds limits of
${this.customImageMaxSize}Mb!`);
}
},
};
app.$mount('#app');

Nuxt JS load components depending on API response

I'm building a nuxt app to consume the wp rest API. In my fetch method I fetch information about needed components. I can't figure out how to then import all the components and render them. I've tried several methods, but I can't see to make it work.
Here's what works:
<component :is="test" :config="componentList[0]"></component><br>
export default {
async fetch({ store, $axios }) {
await store.dispatch("getPageBySlug", "home");
},
computed: {
test() {
return () => import('~/components/HeroIntro');
}
}
};
Ok so this is easy, nothing special - I could now import the component based on the slug etc. But I need to render multitple components and therefor im doing this:
<component
v-for="component in componentList"
:key="component.acf_fc_layout"
:is="component.acf_fc_layout"
:config="component">
</component>
along with this
export default {
async fetch({ store, $axios }) {
await store.dispatch("getPageBySlug", "home");
},
computed: {
page() {
return this.$store.getters.getPageBySlug("home");
},
componentList() {
return this.page.acf.flexible_content;
},
componentsToImport() {
for(const component of this.componentList) {
() => import('~/components' + component.acf_fc_layout);
}
}
}
};
All I'm getting is
Unknown custom element: HeroIntro - did you register the
component correctly? For recursive components, make sure to provide
the "name" option
How do I archieve what im trying?
edit:
So, after a lot of trying, I could only make it work with using an extra component, "DynamicComponent":
<template>
<component :is="componentFile" :config="config"></component>
</template>
<script>
export default{
name: 'DynamicComponent',
props: {
componentName: String,
config: Object
},
computed: {
componentFile() {
return () => import(`~/components/${this.componentName}.vue`);
}
}
}
</script>
Now in Index.vue
<template>
<main class="container-fluid">
<DynamicComponent
v-for="(component, index) in componentList"
:key="index"
:componentName="component.name"
:config="component"
/>
</main>
</template>
<script>
export default {
components: {
DynamicComponent: () => import("~/components/base/DynamicComponent")
}
I am not sure yet if this is optimal - but for now it works great - any input / opinions would be great!

how to call a method on the component by clicking Vue.js?

I am use component of the dialog window dialog.vue from vue-mdl package
<template>
<div class="mdl-dialog-container" v-show="show">
<div class="mdl-dialog">
<div class="mdl-dialog__title">{{title}}</div>
<div class="mdl-dialog__content">
<slot></slot>
</div>
<div class="mdl-dialog__actions" :class="actionsClasses">
<slot name="actions">
<mdl-button class="mdl-js-ripple-effect" #click.native.stop="close">Close</mdl-button>
</slot>
</div>
</div>
</div>
</template>
<script>
import mdlButton from './button.vue'
import createFocusTrap from 'focus-trap'
export default {
components: {
mdlButton
},
computed: {
actionsClasses () {
return {
'mdl-dialog__actions--full-width': this.fullWidth
}
}
},
data () {
return {
show: false
}
},
props: {
title: {
type: String
},
fullWidth: Boolean
},
mounted () {
this._focusTrap = createFocusTrap(this.$el)
},
methods: {
open () {
this.show = true
this.$nextTick(() => this._focusTrap.activate())
this.$emit('open')
},
close () {
this.show = false
this._focusTrap.deactivate()
this.$emit('close')
}
}
}
</script>
I want to bring a dialog window to the other component
<mdl-dialog></mdl-dialog>
<button class="mdl-button mdl-js-button mdl-button--raised">Click me</button>
I found no information on how to call a method of one component within the other. All examples are mainly used props. Tell me how to do it?
How can I call a method open() in <mdl-dialog></mdl-dialog>?
Since they're not parent child you'd want to use an event bus. Since you're using .vue files you can create a file called bus.js like
import Vue from 'vue'
export default new Vue()
Then, import that wherever you need to emit and listen for centralized events. Here's a quick example:
// SomeComponent.vue
import bus from './bus.js'
export default {
methods: {
log (msg) {
console.log(msg)
}
},
created () {
bus.$on('someEvent', this.log)
}
}
Then in another component you can do like...
// AnotherComponent.vue
import bus from './bus.js'
export default {
methods: {
emitClick (msg) {
bus.$emit('Hello from AnotherComponent.vue')
},
},
}
You can read up a bit more about it here: https://v2.vuejs.org/v2/guide/components.html#Non-Parent-Child-Communication
You can create below helper method in methods in your parent component:
getChild(name) {
for(let child of this.$children) if (child.$options.name==name) return child;
},
And call child component method in this way:
this.getChild('mdl-dialog').open();
I don't test it for Vue>=2.0