I have following project structure (index.vue):
<template>
<div class="container">
<navbar></navbar>
<social-media-bar></social-media-bar>
<main>
<home></home>
<news></news>
<vision></vision>
<event-section></event-section>
<artwork></artwork>
<about></about>
<donate></donate>
<contact></contact>
<partners></partners>
</main>
<footer-component></footer-component>
</div>
</template>
I want to change the app-language from inside navbar.vue:
<template>
<div class="nav-bar">
<fa class="icon-locale" #click="toggleLocale" :icon="[ 'fa', 'language' ]" size="2x"></fa>
<div class="locale-menu" :class="{ locale__open: isActiveLocale }">
<p #click="toggleLocale(); setLocale('en');">en</p>
<p #click="toggleLocale(); setLocale('de');">de</p>
<p #click="toggleLocale(); setLocale('ar');">ar</p>
</div>
</div>
</template>
<script setup>
import {ref} from "vue";
import {createI18n} from 'vue-i18n';
const isActiveLocale = ref(false);
const toggleLocale = () => {
isActiveLocale.value = !isActiveLocale.value;
}
const i18n = createI18n({});
const setLocale = (locale) => {
i18n.global.locale = locale
};
</script>
Basically this opens a locale menu with en, de, ar locales which start an #click event that changes i18n.global.locale accordingly.
I need to set the newly set i18n.global.locale in the home component.
home.vue:
<template>
<section id="home" class="home">
<h2>{{ state.heading[state.locale] }}</h2>
</section>
</template>
<script setup>
import {reactive} from 'vue';
import {useI18n} from 'vue-i18n';
const {locale} = useI18n()
const loc = locale.value.substring(0, 2)
const state = reactive({
locale: loc
})
</script>
What I want is to get the newly set i18n.global.locale from navbar.vue into state.locale in home.vue reactively. Since navbar and home are no parent/child, do I have to build an EventBus for this or is there a more elegant solution, maybe with the i18n library?
Edit:
This is the function, that is supposed to change locale globally but it only sets it inside i18n and it looks like the only reactivity possible with that is with i18n's messages, which I am not using.
const setLocale = () => {
i18n.global.locale = 'de'
console.log(i18n.global.locale);
};
I need to change the locale string globally, so that I can use it in all components reactively.
i18n.js
import { createI18n } from "vue-i18n";
export const i18n = createI18n({
locale: "en",
messages: {
en: {
message: {
language: "Language",
hello: "hello world!"
}
},
ja: {
message: {
language: "言語",
hello: "こんにちは、世界!"
}
}
}
});
main.js
import { createApp } from "vue";
import App from "./App.vue";
import { i18n } from "./i18n";
createApp(App).use(i18n).mount("#app");
App.vue
<template>
<button #click="switch">switch to ja</button>
<p>{{ $t("message.hello") }}</p>
</template>
<script>
export default {
name: "App",
methods: {
switch() {
// $i18n is reactively
this.$i18n.locale = "ja";
},
},
};
</script>
I found a super easy way to pass around variables between components in vue with sessionStorage. Just define:
sessionStorage.whatever = 'whateverYouWant'
and you can use sessionStorage.whatever in all your components. Other than localStorage, sessionStorage will reset, when the browser/tab is closed.
I am using it to pass around the selected locale.
Related
I'm trying to create a BaseOverlay component that basically teleports its content to a certain area of my application. It works just fine except there's an issue when using it with v-show... I think because my component's root is a Teleport that v-show won't work because Teleport is a template.
I figured I could then use inheritAttrs: false and v-bind="$attrs" on the inner content... this throws a warning from Vue saying Runtime directive used on component with non-element root node. The directives will not function as intended. It results in v-show not working on MyComponent, but v-if does work.
Any clues as to what I'm doing wrong?
Example
App.vue
<script setup>
import MyComponent from "./MyComponent.vue";
import {ref} from "vue";
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div id="overlays" />
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Doesn't work" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
BaseOverlay.vue
<template>
<Teleport to="#overlays">
<div
class="overlay-container"
v-bind="$attrs"
>
<slot />
</div>
</Teleport>
</template>
<script>
export default {
name: "BaseOverlay",
inheritAttrs: false,
};
</script>
MyComponent.vue
<template>
<BaseOverlay>
{{text}}
</BaseOverlay>
</template>
<script>
import BaseOverlay from "./BaseOverlay.vue";
export default {
name: "MyComponent",
components: {
BaseOverlay
},
props: {
text: {
type: String,
default: ""
}
}
}
</script>
I would consider moving the modal/overlay dependency out of the component and into the app composition to make it more reusable.
Note the isMounted check check - this is to add a delay if the outlet containers have not yet been defined. You may need to add additional handling if your outlets are not pressent on mount e.g. <div id="outlet" v-if="false">
const {
createApp,
defineComponent,
ref,
onMounted
} = Vue
//
// Modal
//
const MyModal = defineComponent({
name: 'MyModal',
props: {
to: {
type: String,
required: true
},
show: Boolean
},
setup(){
const isMounted = ref(false);
onMounted(()=> isMounted.value = true )
return { isMounted }
},
template: `
<teleport :to="to" v-if="isMounted">
<div class="overlay-container" v-show="show">
<slot />
</div>
</teleport>
`
})
//
// Component
//
const MyComp = defineComponent({
name: 'MyComp',
template: `<div>Component</div>`
})
//
// App
//
const MyApp = defineComponent({
name: 'MyApp',
components: {
MyModal,
MyComp
},
setup() {
const modalShow = ref(false);
const modalOutlet = ref('#outletOne');
const toggleModalShow = () => {
modalShow.value = !modalShow.value
}
const toggleModalOutlet = () => {
modalOutlet.value = modalOutlet.value == '#outletOne'
? '#outletTwo'
: '#outletOne'
}
return {
toggleModalShow,
modalShow,
toggleModalOutlet,
modalOutlet,
}
},
template: `
<div>
<button #click="toggleModalShow">{{ modalShow ? 'Hide' : 'Show' }} Modal</button>
<button #click="toggleModalOutlet">Toggle Modal Outlet {{ modalOutlet }} </button>
</div>
<MyModal :to="modalOutlet" :show="modalShow">
<MyComp />
</MyModal>
<div id="outletOne">
<h2>Outlet One</h2>
<!-- outlet one -->
</div>
<div id="outletTwo">
<h2>Outlet Two</h2>
<!-- outlet two -->
</div>
`
})
//
// Assemble
//
const app = createApp(MyApp)
app.mount('body')
/* just some styling */
#outletOne { color: tomato; }
#outletTwo { color: olive; }
h2 { margin: 0; }
[id*=outlet]{ display: inline-flex; flex-direction: column; padding: 1rem; }
button { margin: 1rem; }
<script src="https://unpkg.com/vue#3.1.4/dist/vue.global.js"></script>
Wanted to follow-up on this. I started running into a lot of other issues with Teleport, like the inability to use it as a root element in a component (like a Dialog component because I want to teleport all dialogs to a certain area of the application), and some other strange issues with KeepAlive.
I ended up rolling my own WebComponent and using that instead. I have an OverlayManager WebComponent that is used within the BaseOverlay component, and every time a BaseOverlay is mounted, it adds itself to the OverlayManager.
Example
OverlayManager.js
export class OverlayManager extends HTMLElement {
constructor() {
super();
this.classList.add("absolute", "top-100", "left-0")
document.body.appendChild(this);
}
add(element) {
this.appendChild(element);
}
remove(element) {
this.removeChild(element);
}
}
customElements.define("overlay-manager", OverlayManager);
BaseOverlay.vue
<template>
<div class="overlay-container" ref="rootEl">
<slot />
</div>
</template>
<script>
import {ref, onMounted, onBeforeUnmount, inject} from "vue";
export default {
name: "BaseOverlay",
setup() {
const rootEl = ref(null);
const OverlayManager = inject("OverlayManager");
onMounted(() => {
OverlayManager.add(rootEl.value);
});
onBeforeUnmount(() => {
OverlayManager.remove(rootEl.value);
});
return {
rootEl
}
}
};
</script>
App.vue
<script setup>
import {OverlayManager} from "./OverlayManager.js";
import MyComponent from "./MyComponent.vue";
import {ref, provide} from "vue";
const manager = new OverlayManager();
provide("OverlayManager", manager);
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Now Works" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
<style>
.absolute {
position: absolute;
}
.top-100 {
top: 100px;
}
.left-0 {
left: 0;
}
</style>
This behaves exactly how I need it, and I don't have to deal with the quirks that Teleport introduces, and it allows me to have a singleton that is in charge of all of my overlays. The other benefit is that I have access to the parent of where BaseOverlay is initially added in the HTML (not where it's moved). Honestly not sure if this is a good practice, but I'm chuffed at how cute this is and how well Vue integrates with it.
I'm making a Vue3 Single File Component for a custom list. In my single file component, I want to export the main default Vue component, but also the enum declaring what type of list it is:
child:
<template>
<Listbox>
<template #header>
<h5>{{listType}}</h5>
</template>
</Listbox>
</template>
<script lang="ts">
export enum PagesListType {
RecentlyCreated = 'Recently Created',
RecentlyModified = 'Recently Modified',
Starred = 'Starred'
};
export default {
props: {
listType: PagesListType
},
data() {
return {
pages: [],
PagesListType
};
},
};
</script>
The enum only makes sense within the context of this component, so I don't want to put it in some other folder of types. It only relates to the behavior of this list. But when I try to do this in the parent component, it fails:
parent:
<template>
<div>
<PagesList :listType="PagesListType.RecentlyCreated"></PagesList>
<PagesList :listType="PagesListType.RecentlyModified"></PagesList>
<PagesList :listType="PagesListType.Starred"></PagesList>
</div>
</template>
<script lang="ts">
import PagesList, { PagesListType } from './PagesList.vue';
export default {
//parent component details
};
</script>
When I import the named PagesListType enum, it is just undefined. What do I need to do to export the named enum correctly? Thanks!
I opine you need to export enum into a separate file and import it in different files to use it. Where do you put this file depends on you mainly, how you want to structure your project.
For instance, types.ts file in the src folder can define and export the enum like:
export enum PagesListType {
RecentlyCreated = 'Recently Created',
RecentlyModified = 'Recently Modified',
Starred = 'Starred'
}
you can use the enum anywhere by importing it like:
import { PagesListType } from '#/types';
you have to use #/ instead of src/. because of the src folder to # in your TypeScript configuration available in the tsconfig.json file.
I was able to kinda get this to work by not exporting the enum, but adding it as a property to the exported default component:
child:
enum PagesListType {
RecentlyCreated = 'Recently Created',
RecentlyModified = 'Recently Modified',
Starred = 'Starred'
};
export default {
props: {
listType: PagesListType
},
PagesListType,
data() {
return {
pages: [],
PagesListType
};
},
};
parent:
<template>
<div>
<PagesList :listType="created"></PagesList>
<PagesList :listType="modified"></PagesList>
<PagesList :listType="starred"></PagesList>
</div>
</template>
<script lang="ts">
import PagesList from './PagesList.vue';
export default {
computed: {
created() {
return PagesList.PagesListType.RecentlyCreated;
},
modified() {
return PagesList.PagesListType.RecentlyModified;
},
starred() {
return PagesList.PagesListType.Starred;
}
},
//other parent implementation details omitted
};
</script>
I have a component named ProductArea which displays products loaded from the Prismic API. The products loaded are dependant on a category which is selected by the user in a sidebar.
I'm using Vuex and struggling to come up with a flow that avoids a situation where category is not yet available in my store (category is also loaded from Prismic).
Here is what the parent of ProductArea looks like:
<template>
<div>
<NavBar />
<!-- <Header /> -->
<main>
<div v-if="!$fetchState.pending" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex-1 min-w-0 bg-white xl:flex">
<Sidebar :navigation="navigation" />
<ProductArea />
</div>
</div>
</main>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import NavBar from '#/components/NavBar.vue'
import Sidebar from '#/components/Sidebar.vue'
import Header from '#/components/Header.vue'
import CategoryHeader from '#/components/CategoryHeader.vue'
import ProductGrid from '#/components/ProductGrid.vue'
import { mapActions } from 'vuex'
import { mapGetters } from 'vuex'
export default {
name: 'App',
components: {
Sidebar,
NavBar,
Header,
CategoryHeader
},
data() {
return {
navigation: null
}
},
async fetch() {
const component = this
await this.fetchCategories()
.then(function(navigationResult) {
const navigation = component.$store.getters.navigation
component.navigation = navigation
})
},
fetchOnServer: true,
methods: {
...mapActions({ fetchCategories: 'fetchCategories', fetchProducts: 'fetchProducts' })
}
}
</script>
I assumed having v-if="!$fetchState.pending" would prevent ProductArea from being created until category has been loaded into the store, however this doesn't seem to be the case.
Here is ProductArea:
<template>
<div class="bg-white lg:min-w-0 lg:flex-1">
<CategoryHeader :category="this.category" :products="this.products" />
<div class="sm:p-6">
<ProductGrid :category="this.category.primary.category" :products="this.products" />
</div>
</div>
</template>
<script lang="ts">
import { mapActions } from 'vuex'
import { mapGetters } from 'vuex'
import Locale from '#/types/locale'
export default {
name: 'ProductArea',
data() {
return {
category: this.$store.getters.category,
products: Array
}
},
async fetch() {
const component = this
await this.fetchProducts(this.category)
.then(function(productsResult) {
const products = component.$store.getters.products
component.products = products
console.log(products)
})
},
fetchOnServer: true,
methods: {
...mapActions({ fetchProducts: 'fetchProducts' })
}
}
</script>
Here's the error I'm receiving:
Error in fetch(): TypeError: Cannot read property 'products' of undefined
This error is referring to the undefined category within the fetchProducts called via fetch on the ProductsArea component.
Can anyone point me in the right direction? What would be the optimal flow here to prevent category being accessed before it is available?
You could set a default category. If you don't want to do that, bring the Vuex category into the parent and only show <ProductArea> when it's defined:
Parent
<ProductArea v-if="category" />
computed: {
...mapGetters(['category'])
}
This is necessary because your v-if on $fetchState.pending only tests whether all the categories are loaded, but for the child component you also need to test that a category has been selected.
In fact, you can simplify all your code by mapping the getters instead of storing getters in variables, which is not a good practice. Those variables wouldn't be updated reactively when the getter changes. Instead, completely remove the data options from both components:
Parent
async fetch() {
await this.fetchCategories();
}
computed: {
...mapGetters(['category', 'navigation'])
}
Child
async fetch() {
await this.fetchProducts();
}
computed: {
...mapGetters(['category', 'products'])
}
Other improvements:
You can shorten the mapActions calls a bit:
Parent: ...mapActions(['fetchCategories'])
Child: ...mapActions(['fetchProducts'])
I want to store input-value from App.vue, and use it in another component. How can I do it? I don't need the show the value in the template, I just need the value inside other components function. In JS I could just use a global var, but how can I achieve it in Vue?
App.vue:
<template>
<div id='app'>
<!-- App.vue has search bar -->
<b-form-input #keydown='search' v-model='input'></b-form-input>
<div>
<!-- Here's my other components -->
<router-view />
</div>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
input: '',
value: ''
}
},
methods: {
search () {
this.value = this.input
this.input = ''
}
}
}
</script>
Another component:
<template>
<div>
<p>I'm another component</p>
<p>App.vue input value was: {{value}} </p>
</div>
</template>
<script>
export default {
props: ['value'],
data () {
return {
value: ''
}
}
}
</script>
This is the basic logic I'm trying to achieve. Input value in App.vue --> anotherComponent.vue
If components are not parent and child you can use store for this:
More advanced vuex store that should be your default GO TO - NPM.
Or simple solution with js object.
a. Create store.js file and export object with property in which you will store value.
b. Import store.js object to vue scripts and use it simply like:
import Store from 'store.js'
Store.value
I need to build list of dynamical components that I can group in group component. Then I need to send all information about builded components and groups.
I can use <component v-for="componentName in myComponents" :is="componentName"></component>, and get information about components using this.$children.map(component => component.getInformation()), but then I can't move some component to group component, because I have only component name not the component instance with data (it just render with default data).
I also can use this:
<template>
<div ref="container"> </div>
</template>
<script>
import someComponent from 'someComponent.vue'
import Vue from 'vue'
export default {
data () {
return {
myComponents: []
}
},
methods: {
addSomeComponent () {
let ComponentClass = Vue.extend(someComponent);
let instance = new ComponentClass({});
myComponents.push(instance);
instance.$mount();
this.$refs.container.appendChild(instance.$el)
},
getInformation () {
return this.myComponents.map(component => component.getInformation());
}
}
}
</script>
But then I can't use reactivity, directives (e.g. directives for drag and drop), and it's not data driven pattern.
Any suggestions?
<template>
<div class="component">
<template v-for="(child, index) in children()">
<component :is="child" :key="child.name"></component>
</template>
</div>
</template>
<script>
import someComponent from 'someComponent.vue'
import Vue from 'vue'
export default {
methods: {
children() {
let ComponentClass = Vue.extend(someComponent);
let instance = new ComponentClass({});
return [
instance
];
},
}
}
</script>