Reference variables between components in Vue - vue.js

I have a sidebar component that onclick toggles between closed and expanded, using the following functions:
const is_expanded = ref(localStorage.getItem("is_expanded") === "true")
const ToggleMenu = () => {
is_expanded.value = !is_expanded.value
// #ts-ignore
localStorage.setItem("is_expanded", is_expanded.value)
}
The value of is_expanded determines which class the component uses:
<template>
<aside :class="`${is_expanded ? 'is-expanded' : ''}`">
<div class="logo">
<img :src="logoURL" alt="Vue" />
</div>
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="ToggleMenu">
<span class="material-icons">keyboard_double_arrow_right</span>
</button>
</div>
.........
In the middle of the screen I have an iframe component which currently has a fixed margin-left so that it starts where the expanded sidebar ends, however I would like it to be dynamic and re-size when the sidebar is closed.
Is there a way I can reference the "is_expanded" variable from the sidebar component within the iframe component?

Related

Why is v-model not working properly, returning'' Property "modelValue" was accessed during render but is not defined on instance.'' Vue 3

The Component I am making is a basic toggle switch made in vue using tailwind. during the initial render the switch is in an incorrect position, first click toggles the theme but the switch still stays the same, third click and forward the switch works properly
Switch.vue
<template>
<label class="switch relative inline-block w-[39px] h-[19px]">
<input type="checkbox" class="w-0 h-0 opacity-0" v-model="modelValue">
<span
class="absolute rounded-[34px] before:rounded-[50%] cursor-pointer top-0 left-0 right-0 bottom-0 bg-gray-300 transition shadow-inner before:absolute before:h-[17px] before:w-[17px] before:left-[1px] before:top-[1px] before:bg-white before:transition "
:class="[{
'before:translate-x-5 bg-blue-500': modelValue
}]" #click="$emit('update:modelValue', modelValue)"></span>
</label>
</template>
Parent.vue
// darkTheme comes from a pinia store.
<Switch v-model="darkTheme.enableDarkTheme" />
I was expecting the switch to work with v-model and toggle the theme, it works after a few clicks but first 3 clicks it does not toggle.
import { defineStore } from "pinia";
export let useThemeStore = defineStore('darkTheme', {
state: () => {
return {
enableDarkTheme: window.matchMedia('(prefers-color-scheme: dark)').matches,
}
},
actions: {
toggle() {
!this.enableDarkTheme;
}
}
})
Edit: Pinia seems to be sending the prop value as undefined on the first mutation, which seems to be causing the issue. please verify the store code
Referring to custom components v-model in the docs
https://vuejs.org/guide/components/v-model.html
for v-model in custom components props and emits need to be explicitly defined in the script setup, also see #Estus's comment about nested v-model
This is a simple vue component for a toggle slider
Switch.vue
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<label class="switch relative inline-block w-[39px] h-[19px]">
<input type="checkbox" class="w-0 h-0 opacity-0" :checked="modelValue">
<span
class="absolute rounded-[34px] before:rounded-[50%] cursor-pointer top-0 left-0 right-0 bottom-0 bg-gray-300 transition shadow-inner before:absolute before:h-[17px] before:w-[17px] before:left-[1px] before:top-[1px] before:bg-white before:transition "
:class="[{
'before:translate-x-5 bg-blue-500': modelValue
}]" #click="$emit('update:modelValue', !modelValue)"></span>
</label>
</template>
Parent Component
//Inside the parent component
<Switch v-model="darkTheme.enableDarkTheme" />

Vue Composition API with "is" attribute for conditional rendering

I'm working on an onboarding process that will collect a users name, location, job , etc. It needs to be one question per page but as an SPA so I currently have around 20 components to conditionally render.
Same problem as this but I've been asked change to Composition API and now I can't get this to work.
Vue - Render new component based on page count
The solution in the above was to make an array with all the page titles, a for loop and use :is to render each page as needed.
My components are named in this format: MLandingPage.vue, MFirstName.vue, etc.
I also have buttons that add or minus 1 from onboardingStep to go forward or back a step.
I have tried this:
const onboardingPages = ref(["MLandingPage", "MFirstName", 'MWelcome', 'MLastName', 'MAge' ]);
const onboardingStep = ref(0);
<template v-for="(onboardingPage, index) in onboardingPages" :key="index">
<component :is="onboardingPage" v-if="index === onboardingStep"/>
</template>
This doesn't render anything on the page and when I inspect, it just has <mlandingpage></mlandingpage> with no content.
I tried this instead:
const onboardingPages = ref(["m-landing-page", "m-first-name", 'm-welcome', 'm-last-name', 'm-age' ]);
Still nothing and I get this when I inspect the page: <m-landing-page></m-landing-page>
As a test, if I just write <m-landing-page></m-landing-page> in the code, it works.
Totally new to Composition API and the "is" attribute so any help would be great.
Edit - added more code for context:
<script setup>
import MLandingPage from "~~/components/onboarding/MLandingPage.vue";
import MFirstName from "~/components/onboarding/MFirstName.vue";
import MLastName from "~/components/onboarding/MLastName.vue";
import MAge from "~/components/onboarding/MAge.vue";
import { ref } from "vue";
const onboardingPages = ref(["m-landing-page", "m-first-name", 'm-welcome', 'm-last-name', 'm-age' ]);
const onboardingStep = ref(0);
function prevStep() {
onboardingStep.value -= 1;
}
function nextStep() {
onboardingStep.value += 1;
}
</script>
<template>
<div class="flex items-center justify-end gap-2 md:gap-10 mt-16 px-5 md:px-20">
<h1>Example Page Title</h1>
<div class="mt-16 p-5 pr-8 md:px-20 md:mt-24">
<template v-for="(onboardingPage, index) in onboardingPages" :key="index">
<component :is="onboardingPage" v-if="index === onboardingStep"/>
</template>
</div>
<button #click="prevStep">Back</button>
<button #click="nextStep">Next</button>
</div>
</template>
Thanks!
Found a much simpler way around this
Key issue was that I had the page names as strings
const onboardingPages = ref([MLandingPage, MFirstName, MWelcome, MLastName, MAge]);
<div class="mt-16 p-5 pr-8 md:px-20 md:mt-24">
<transition name="fade">
<component :is="onboardingPages[onboardingStep]" />
</transition>
</div>

<MenuItems /> is missing a parent <Menu /> component error (with MenuItems in slot)

I am creating a component in Vue3 called <Dropdown>.
The Dropdown component uses other components in turn: Menu, MenuItems, MenuItem and MenuButton from the headlessui/vue library.
My idea is that you can put any content in the Dropdown, that's why I created a slot called #contentdropdown.
The problem is that when I pass this slot content to the Dropdown component, Vue gives me the following error:
< MenuItems /> is missing a parent < Menu /> component
This is my Dropdown componente code:
<template>
<Menu as="div" class="relative inline-block text-left">
<div>
<MenuButton class="btn inline-flex justify-center w-full text-sm" :class="'btn-'+variant">
Options
<ChevronDownIcon class="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
</MenuButton>
</div>
<transition enter-active-class="transition ease-out duration-100" enter-from-class="transform opacity-0 scale-95" enter-to-class="transform opacity-100 scale-100" leave-active-class="transition ease-in duration-75" leave-from-class="transform opacity-100 scale-100" leave-to-class="transform opacity-0 scale-95">
<slot name="contentdropdown"></slot>
</transition>
</Menu>
</template>
<script>
import { Menu, MenuButton } from '#headlessui/vue'
import { ChevronDownIcon } from '#heroicons/vue/solid'
import { vVariantProp } from '../../../../constants'
import { reactive, computed } from 'vue';
export default {
name: 'dropdown',
props: {
...vVariantProp,
},
setup(props) {
props = reactive(props);
return {
}
},
};
</script>
Why does it need the parent component called Menu?, if in fact I am already painting the slot inside the component and also importing it inside the Dropdown component.
This is how I pass to the Dropdown component the content through its #contentdropdown slot:
<Dropdown v-bind="{'variant':'primary'}">
<template #contentdropdown>
<MenuItems class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<div class="py-1">
<MenuItem>
Subitem1
</MenuItem>
<MenuItem>
Subitem2
</MenuItem>
</div>
</MenuItems>
</template>
</Dropdown>
The error <MenuItems /> is missing a parent <Menu /> component is not a Vue specific error. It is an error thrown by headlessui/vue - source
MenuItems component (as well as MenuButon etc - see doc) is designed to be used inside Menu component. It is using inject to tap into state provideded by the Menu component. There is nothing you can do about it - it is designed that way
Problem is that slot content (everything inside <template #contentdropdown> in the last code example) in Vue is always rendered in parent scope
Everything in the parent template is compiled in parent scope; everything in the child template is compiled in the child scope.
This means that MenuItems rendered as slot content has no access to data provideded by the Menu component rendered inside your Dropdown component
I don't see any way to overcome this limitation. You'll need to change your design (or describe your use case to headlessui/vue maintainers and ask them to implement alternative approach to share MenuContext with child components - for example using slot props)

Vue - display/create component from a function

One thing that I have been struggling to figure out how to do better is modals. Currently, I am registering the modal component on each Vue that needs it. However, this feels rather sloppy, as I am having to register the component several times. Even using mix-ins just does not feel like an elegant solution. What would be optimal to be able to do is to mimic JavaScript's alert() method on the Vue instance. For example, be able to call this.ShowModal(Header, Body)
However, from my understanding, there is no way to accomplish this
Take for example my Modal example. You could have a modal template like this:
<script type="text/x-template" id="modal-template">
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
default header
</slot>
</div>
<div class="modal-body">
<slot>
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button class="modal-default-button" #click="$emit('close')">
OK
</button>
</slot>
</div>
</div>
</div>
</div>
</transition>
</script>
Then you would have to reference the component over and over again like this
<template>
<button #click="displayModal">Display the Modal Alert</button>
<modal v-if="showModal" #close="showModal = false">
<h3 slot="header"> This is a good header </h3>
<p>
Look at me I am the body! You have seen me {{displayCount}} times!
</p>
</modal>
</template>
<script>
components: {modal},
data: {
showModal: false,
displayCount: 0
},
methods: {
displayModal(){
this.displayCount++
this.showModal = true;
}
}
</script>
If you wanted to reuse the component for several messages from within the parent you would then have to add several more variables to store things such as the header and body. You could put some of the logic into a mixin but you would still have to have the clutter of adding the modal component and possibly the mixin.
This brings us to the question. Is there a way to create a function in the Vue instance that would allow for us to dynamically create a Modal component and fill in the slots with arguments passed to the function? e.g. this.ShowModal("This is a good header", "Look at me I am the body!")
Use Vue.extend() create a "modal" constructor and create a instance,you can mount it to DOM dynamically by $mount or other ways
In Modal example:
modal.vue:
<template>
<div>
{{message}} //your modal content
</div>
</template>
<script>
export default {
name: 'modal',
data(){
return {
message: '',
}
},
methods:{
/************/
close () {
/****this.$destroy()****/
}
}
}
</script>
modal.js:
import myModal from 'modal.vue'
import Vue from 'vue'
const modalConstructor = Vue.extend(myModal)
const modal = (options,DOM)=>{
const modalInstance = new modalConstructor({data:options})
modalInstance.vm = modalInstance.$mount() //get vm
const dom = DOM || document.body // default DOM is body
dom.appendChild(modalInstance.vm.$el) // mount to DOM
return modalInstance.vm
}
export default modal
now you can create a Modal component by a function like this:
import showModal from 'modal.js'
showModal({message:"..."})

Presentation Component in Vue2

I want to display all my forms and info pages in floating sidebox.
I don't want to copy and paste the floating sidebox html to all the places. So I want to create a component which acts as container to my forms or info pages.
This is the sample code for form.
<div class="floating-sidebox">
<div class="sidebox-header">
<div class="sidebox-center">
<h3 class="title">{{ title }}</h3>
</div>
</div>
<div class="sidebox-content">
<div class="sidebox-center">
<!-- This is the actual content. Above container code is common for all forms. -->
<vue-form-generator :schema="schema" :model="model" :options="{}"></vue-form-generator>
</div>
</div>
<div class="floating-sidebox-close" #click="cancel"></div>
</div>
<div class="floating-sidebox-overlay"></div>
In above code, I uses vue-form-generator to generate the form. The floating-sidebox elements are common for all forms and info pages. I want to abstract it by Presentational component.
How could I do it Vue2?
Define a component that wraps all your "floating-sidebox" components. You can access the "floating-sideboxes" via this.$children and use their title etc. as navigation placeholder. Since $children is an array you can easily represent the currently visible entity with and index
...
data: function() {
sideboxes: [],
currentIndex: null
},
...
mounted: function() {
this.sideboxes = this.$children;
this.currentIndex= this.$children.length > 0 ? 0 : null;
},
...
computed: {
current: function() {
return this.currentIndex ? this.sideboxes[this.currentIndex] : null;
}
}
You can then bind in the template of the wrapping view
<ul>
<li v-for="(sidebox, index) in sideboxes" #click="currentIndex = index"><!-- bind to prop like title here --></li>
</ul>
<div>
<!-- binding against current -->
</div>
JSfiddle with component https://jsfiddle.net/ptk5ostr/3/