Vuejs - Move header section of template to parent - javascript

I am trying to create a template with two parts
the tab (slot) -> could only get the slot to work with using a ref
The content (slot)
This component(tab) is wrapped in a component(tabs aka parent) that organizes the tabs based on certain props.
The overall goal is to create something like so:
https://getbootstrap.com/docs/4.0/components/navs/#tabs
Except with the ability to have custom tabs. For simplicity, I want to keep all the information relating to the tab within the tab component
1 - the header is not rendered in the component itself but pushed to the parent ***
2 - the tab component pushes the $ref to the parent and then the parent renders the HTML and listeners
How can i push(or another method to pass the information to the parent) data to the parent and keep all the listeners and js associated with the components in the tab slot
//tab component
<template>
<div>
<div class="tab" ref="tab">
<slot name="heading"> //-> Only available in setup context.slots if the default content is not used therefore resulted in using ref
//Default content
{{heading}} //-> if I add content to the heading slot via different components, the JS/listeners associated to those components do not work. I assume because I'm only pushing the HTML
</slot>
</div>
<div class="content ">
<slot/>
</div>
<div>
</template>
<script>
import {onMounted, ref} from '#nuxtjs/composition-api'
setup(props, {parent}){
const tab = ref()
onMounted(()=>{
let tab = {
data: tab.value //The entire ref
//data: tab.value.innerHTML => Works for passing the html but no listeners or js work
}
//parent has data.tabs = []
parent.data.tabs.push(tab)
})
return {
tab
}
},
props:{
....
}
</script>
//tab parent component (part to render tab via data.tabs)
<ul
>
<li
v-for="(child, index) in data.tabs"
class="s-tabs--li"
v-bind:key="index"
v-html="child.data"
></li>
</ul>
//Used in action
<s-tabs>
<s-tab heading="Home">
<div>
Home
</div>
</s-tab>
<s-tab heading="Service" icon="flower" tag="music">
<div>
Service
</div>
</s-tab>
</s-tabs>

Related

Using vanilla js append, vue.js loses refrence

Ill simplify the example. Basically i have multiple widgets on one page and i tought it would be a good practice not to copy all the widgets into v-dialog but use the refrence and apenned them into the dialog and back to the grid when needed. The problem is when i append my html into dialog and I try to run this.$refs vue loses track of infinite-loading componente... this.$refs does not contain ref="infinitiveLoading". If some1 can explain and maybe reccommend better practice.. thx
<div>
<div id="item_containerTest" ref="item_containerTest">
<span>Hello world</span>
<infinite-loading
ref="infinitiveLoading"
v-show="items.length !== 0 || this.loading"
#infinite="infiniteHandler"
>
<div slot="no-more"></div>
</infinite-loading>
</div>
<v-dialog v-model="scheduleDialog" id="dialog" ref="dialog"> </v-dialog>
</div>
//ignore itemID and columnID, i need them so i can append item back to the grid after dialog closes
openFullScreenDialog(itemId, columnId, title){
itemContainer = document.getElementById(`item_container${title}`);
dialog = document.getElementById("dialog");
dialog.append(itemContainer);
}
As Lucero said in the comments, you shouldn't manipulate the DOM with the classic javascript API. The breaks the shadow DOM of Vuejs (i.e. a runtime copy of the current DOM state in memory) and thus, its reactivity and components references.
If you have a content that have to be wrapped in different container depending on a prop for example, you can use slots for this :)
<template>
<div>
<div v-if="dialog">
<slot>
</div>
<v-dialog v-else v-model="openDialog">
<slot>
</v-dialog>
</div>
</template>
<script lang="ts">
import { Vue, Component } from 'nuxt-property-decorator'
export default Vue.extends({
name: 'GridOrDialog',
props: {
dialog: { type: Boolean, default: false }
},
data() {
return {
openDialog: true,
}
}
})
</script>
This way, you just have to declare the content once, and it will be on a v-dialog if you sets the prop dialog to true.
<GridOrDialog dialog="isDialogMode">
<span>Hello world</span>
<infinite-loading
ref="infinitiveLoading"
v-show="items.length !== 0 || this.loading"
#infinite="infiniteHandler"
>
<div slot="no-more"></div>
</infinite-loading>
</GridOrDialog>

How to access $children in Vue 3 for creating a Tabs component?

I'm trying to create a Tabs component in Vue 3 similar to this question here.
<tabs>
<tab title="one">content</tab>
<tab title="two" v-if="show">content</tab> <!-- this fails -->
<tab :title="t" v-for="t in ['three', 'four']">{{t}}</tab> <!-- also fails -->
<tab title="five">content</tab>
</tabs>
Unfortunately the proposed solution does not work when the Tabs inside are dynamic, i.e. if there is a v-if on the Tab or when the Tabs are rendered using a v-for loop - it fails.
I've created a Codesandbox for it here because it contains .vue files:
https://codesandbox.io/s/sleepy-mountain-wg0bi?file=%2Fsrc%2FApp.vue
I've tried using onBeforeUpdate like onBeforeMount, but that does not work either. Actually, it does insert new tabs, but the order of tabs is changed.
The biggest hurdle seems to be that there seems to be no way to get/set child data from parent in Vue 3. (like $children in Vue 2.x). Someone suggested to use this.$.subtree.children but then it was strongly advised against (and didn't help me anyway I tried).
Can anyone tell me how to make the Tab inside Tabs reactive and update on v-if, etc?
This looks like a problem with using the item index as the v-for loop's key.
The first issue is you've applied v-for's key on a child element when it should be on the parent (on the <li> in this case).
<li v-for="(tab, i) in tabs">
<a :key="i"> ❌
</a>
</li>
Also, if the v-for backing array can have its items rearranged (or middle items removed), don't use the item index as the key because the index wouldn't provide a consistently unique value. For instance, if item 2 of 3 were removed from the list, the third item would be shifted up into index 1, taking on the key that was previously used by the removed item. Since no keys in the list have changed, Vue reuses the existing virtual DOM nodes as an optimization, and no rerendering occurs.
A good key to select in your case is the tab's title value, as that is always unique per tab in your example. Here's your new Tab.vue with the index replaced with a title prop:
// Tab.vue
export default {
props: ["title"], 👈
setup(props) {
const isActive = ref(false)
const tabs = inject("TabsProvider")
watch(
() => tabs.selectedIndex,
() => {
isActive.value = props.title === tabs.selectedIndex
} 👆
)
onBeforeMount(() => {
isActive.value = props.title === tabs.selectedIndex
}) 👆
return { isActive }
},
}
Then, update your Tabs.vue template to use the tab's title instead of i:
<li class="nav-item" v-for="tab in tabs" :key="tab.props.title">
<a 👆
#click.prevent="selectedIndex = tab.props.title"
class="nav-link" 👆
:class="tab.props.title === selectedIndex && 'active'"
href="#" 👆
>
{{ tab.props.title }}
</a>
</li>
demo
This solution was posted by #anteriovieira in Vuejs forum and looks like the correct way to do it. The missing piece of puzzle was getCurrentInstance available during setup
The full working code can be found here:
https://codesandbox.io/s/vue-3-tabs-ob1it
I'm adding it here for reference of anyone coming here from Google looking for the same.
Since access to slots is available as $slots in the template (see Vue documentation), you could also do the following:
// Tabs component
<template>
<div v-if="$slots && $slots.default && $slots.default()[0]" class="tabs-container">
<button
v-for="(tab, index) in getTabs($slots.default()[0].children)"
:key="index"
:class="{ active: modelValue === index }"
#click="$emit('update:model-value', index)"
>
<span>
{{ tab.props.title }}
</span>
</button>
</div>
<slot></slot>
</template>
<script setup>
defineProps({ modelValue: Number })
defineEmits(['update:model-value'])
const getTabs = tabs => {
if (Array.isArray(tabs)) {
return tabs.filter(tab => tab.type.name === 'Tab')
} else {
return []
}
</script>
<style>
...
</style>
And the Tab component could be something like:
// Tab component
<template>
<div v-show="active">
<slot></slot>
</div>
</template>
<script>
export default { name: 'Tab' }
</script>
<script setup>
defineProps({
active: Boolean,
title: String
})
</script>
The implementation should look similar to the following (considering an array of objects, one for each section, with a title and a component):
...
<tabs v-model="active">
<tab
v-for="(section, index) in sections"
:key="index"
:title="section.title"
:active="index === active"
>
<component
:is="section.component"
></component>
</app-tab>
</app-tabs>
...
<script setup>
import { ref } from 'vue'
const active = ref(0)
</script>

Angular child route elements outside of router-outlet

I was wondering if there is a way to implement a child component element out of the <router-outlet>, like in the example, I wish each component inside of the outlet could show its own buttons on the parent component.
<div class="header">
<h1>Page Title</h1>
<div class="action-buttons">
<!-- child component buttons -->
</div>
</div>
<div id="wrapper">
<router-outlet></router-outlet>
</div>
I'm not sure if it's the best way but what I'm doing is moving the buttons from the child to the parent using Renderer2 every time the route changes.
this.router.navigate(["/"]).then(() => {
this.moveButtons();
});
private moveButtons() : void {
const parent = document.querySelector('.float-buttons');
const buttonsParent = document.getElementById('childButtons');
parent.childNodes.forEach(child => {
this.renderer.removeChild(parent,child);
});
if(!buttonsParent) return;
const children = buttonsParent.childNodes;
children.forEach(child => {
this.renderer.appendChild(parent, child);
});
}

How to open a modal inside a rendered list in Vue?

I am rendering a list, and have a button that says "Expand". I want that when this button is clicked, it opens a modal with content that is fetched.
I'm trying to render the name inside the data, the same way I'm (correctly) rendering the date property. This can be seen in the following code:
<li v-for="data in tableData" v-bind:key="data">
<button v-on:click="openModal()">{{data.date}}</button>
<sweet-modal ref="modal">{{data.name}}</sweet-modal>
</li>
And the function that opens the modal looks like this:
openModal(){
// let vc = this; I have also tried calling vc.$refs.modal.open()
this.$refs.modal.open()
}
I'm getting a this.$refs.modal.open is not a function and I suspect that it is because this must be used in some clever way in the function that opens the modal.
I think the main issue is that you're rendering a modal component for each list item, which might not be what you want.
What you could do instead is to declare the modal outside of any other element and when you click a list item, you'd store that item in a variable and then you open the modal which will show the contents of that variable.
<template>
<sweet-modal ref='modal'>
{{crtSelectedItem.name}}
</sweet-modal>
<li v-for="data in tableData" v-bind:key="data">
<button v-on:click='openModal(data)'>{{data.date}}</button>
</li>
<!-- ... -->
</template>
export default {
data: () => ({ crtSelectedItem: null }),
methods: {
openModal (item) {
this.crtSelectedItem = { ...item };
this.$refs.modal.open();
}
}
}

Getting the event source from custom events in Vue?

I'm building a Vue component that consists of an unspecified number of child components. Only one child component is visible at all times, and the user can only switch between child components when the one currently visible has emitted an is-valid event.
I want to keep this decoupled, such that children do not know about their parent and only communicate by emitting events. This also means that the children do not know their position within the parent component.
So, the parent component somehow has to keep track of which child the event came from. If the event came from the right child (the one currently visible) then some buttons are activated that allows the user to go to the next or previous child.
Here's my code so far:
HTML
<div id="app">
<template id="m-child">
<div>
<button v-on:click="setstate(true)">Valid</button>
<button v-on:click="setstate(false)">Invalid</button>
</div>
</template>
<template id="m-parent">
<div>
<m-child v-on:newstate="newchildstate"></m-child>
<m-child v-on:newstate="newchildstate"></m-child>
<m-child v-on:newstate="newchildstate"></m-child>
</div>
</template>
<m-parent></m-parent>
</div>
JS
Vue.component('m-child', {
template: '#m-child',
data: function() {
return {};
},
methods: {
setstate: function (valid) {
this.$emit('newstate', valid);
}
}
});
Vue.component('m-parent', {
template: '#m-parent',
methods: {
newchildstate: function (valid) {
console.log('valid:' + valid + ', but where from?');
}
}
});
new Vue({
el: '#app'
});
Of course I could hardcode an index on the child event binding:
<m-child v-on:newstate="newchildstate(0, $event)"></m-child>
<m-child v-on:newstate="newchildstate(1, $event)"></m-child>
<m-child v-on:newstate="newchildstate(2, $event)"></m-child>
But that would make the whole setup a lot less modular, I just want to be able to plug in a number of children in the DOM and make it work right away.
I've looked at the API for Vue events and there doesn't seem to be a way to get the source from the event object.
This depends on what you want to receive back, my personal preference is to pass in a prop to set a unique id and pass it back in the $emit:
<m-child v-on:newstate="newchildstate" :id="1"></m-child>
<m-child v-on:newstate="newchildstate" :id="2"></m-child>
<m-child v-on:newstate="newchildstate" :id="3"></m-child>
Then in your child component you can emit an object with the state and id the id:
Child:
this.$emit('newstate', {id: this.id, state: valid});
Parent:
newchildstate: function (valid) {
console.log('valid:' + valid.state + ', from' + valid.id);
}
I realise that this doesn't look hugely different from your hard coded example, but at some point your parent is going to want to deal with the event, so you could set up an array in data with the initial states and then use a v-for:
data: {
children: [true, false, false] // setup states
}
You would then do:
<div v-for="(state, index) in states">
<m-child v-on:newstate="newchildstate" :id="index"></m-child>
</div>
And in your view model:
methods: {
newchildstate: function(valid) {
this.$set(this.states, valid.id, valid.state);
}
}
Here's a JSFiddle that initiates the array dynamically via a prop and sets up the child components: https://jsfiddle.net/2y9727e2/

Categories