I want to use emit to switch between page components - vue.js

I have a Header and a Main page on Top page now, and I have three Tab functions in the Header component. I want to switch displays(A, B, C) in the Main page when I press each Tab (A, B, C). I am trying to change it using Emit but it doesn't work. If you could give me some advice, I really appreciate it.
Header.vue
<script setup >
const tabs = [
{name: 'A', comp: A},
{name: 'B', comp: B},
{name: 'C', comp: C}
];
interface Emits {
(e: "clicked", value: any);
}
const clicked = defineEmits<Emits>();
</script>
<template>
<div class="tabs" >
<button v-bind:class="['tab-button', A]" #click="$emit('clickedA',tabs[0])">A</button>
<button v-bind:class="['tab-button', B]" #click="$emit('clickedB',tabs[1])">B</button>
<button v-bind:class="['tab-button', C]" #click="$emit('clickedC',tabs[2])">C</button>
</div>
</template>
Mainpage.vue
<script>
let currentTab = A
function getValue(event) {
currentTab = event
}
</ script>
<template>
<Header />
<div>
<component :is="currentTab" #clicked="getValue"></component>
</div>
</template>

I would have the 3 buttons all call the same emit method, passing in the component String as a parameter of that emit:
<button v-bind:class="['tab-button']"
#click="$emit('myClicked', tabs[0])">
A
</button>
<button v-bind:class="['tab-button']"
#click="$emit('myClicked', tabs[1])">
B
</button>
<button v-bind:class="['tab-button']"
#click="$emit('myClicked', tabs[2])">
C
</button>
Then, in the parent component, you could listen for that emit and that one alone:
<Header #myClicked="getValue" />
and in script:
function getValue(tab) {
currentTab.value = tab.name;
}
For example:
Mainpage.vue
<template>
<div>
<Header #myClicked="getValue" />
<div>
<component :is="currentTab"></component>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import Header from "./Header.vue";
let currentTab = ref("A");
function getValue(tab) {
currentTab.value = tab.name;
}
function getTab(tab) {
currentTab.value = tab.name;
}
</script>
Header.vue
<script setup>
const tabs = [{ name: "A" }, { name: "B" }, { name: "C" }];
const emit = defineEmits(["myClicked"]);
</script>
<template>
<div class="tabs">
<button v-bind:class="['tab-button']"
#click="$emit('myClicked', tabs[0])">
A
</button>
<button v-bind:class="['tab-button']"
#click="$emit('myClicked', tabs[1])">
B
</button>
<button v-bind:class="['tab-button']"
#click="$emit('myClicked', tabs[2])">
C
</button>
</div>
</template>
A, B, and C.vue:
<template>
<h2>Component A</h2>
</template>
<template>
<h2>Component B</h2>
</template>
<template>
<h2>Component C</h2>
</template>
main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import A from "./components/A.vue";
import B from "./components/B.vue";
import C from "./components/C.vue";
const app = createApp(App);
app.component("A", A);
app.component("B", B);
app.component("C", C);
app.mount('#app')
OCD cleaning up code a little more.
Using v-for in the Header.vue file to simplify the creation of buttons, and passing the name field into the emit's method parameter changes the following vue files:
Header.vue
<script setup>
const tabs = [
// assuming that in "real" code,
// the display name and component name values would likely be different
{ name: "A", compName: "A" },
{ name: "B", compName: "B" },
{ name: "C", compName: "C" },
];
const emit = defineEmits(["myClicked"]);
</script>
<template>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.name"
#click="$emit('myClicked', tab.compName)"
>
{{ tab.name }}
</button>
</div>
</template>
Mainpage.vue
<template>
<div>
<Header #myClicked="getValue" />
<div>
<component :is="currentTab"></component>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
import Header from "./Header.vue";
let currentTab = ref("A");
function getValue(tabName) {
currentTab.value = tabName;
}
function getTab(tab) {
currentTab.value = tab.name;
}
</script>

Related

How can I accept only one component as slot in vue

I have a button-group component and I want this component only accepts Button component that I created as slots.
So this is my ButtonGroup component:
<template>
<div class="button-group">
<slot />
</div>
</template>
<script lang="ts">
export default {
name: 'ButtonGroup',
components: {
Button,
},
};
</script>
How can I accept only Button component as slot?
use render function
<script>
import {h} from 'vue';
export default {
name: 'ButtonGroup',
render() {
const buttons = []
for (let defaultElement of this.$slots.default()) {
// case: <button />
if (defaultElement.type === 'button') {
buttons.push(defaultElement)
}
// other component
// if (defaultElement.type.name === 'Button') {
// buttons.push(defaultElement)
// }
}
return h('div', {class: 'button-group'}, buttons)
}
};
</script>
I referenced here https://vuejs.org/api/render-function.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<div class="button-group">
<slot name="button">
<Button />
</slot>
</div>
</template>
<script>
export default {
name: 'ButtonGroup',
components: {
Button,
},
};
</script>
//Use of ButtonGroup component`enter code here`
<ButtonGroup>
<template #button />
</ButtonGroup>

Show on click / hide on blur wrapper component

I have several widgets that I'd like to toggle on click/blur/submit.
Let's take a simple example with an input (Vue 2 style)
Input.vue
<template>
<input
ref="input"
:value="value"
#input="input"
#blur="input"
#keyup.escape="close"
#keyup.enter="input"
/>
</template>
<script>
export default {
props: ['value'],
methods: {
input() {
this.$emit("input", this.$refs.text.value);
this.close();
},
close() {
this.$emit("close");
},
}
}
</script>
ToggleWrapper.vue
<template>
<div #click="open = true">
<div v-if="open">
<slot #close="open = false"></slot> <!-- Attempt to intercept the close event -->
</div>
</div>
</template>
<script>
export default {
data() {
return {
open: false,
}
},
}
</script>
Final usage:
<ToggleWrapper>
<Input v-model="myText" #submit="updateMyText" />
</ToggleWrapper>
So when I click on ToggleWrapper it appears, but if I close it, it doesn't disappear because it's not getting the close event.
Should I use scoped events ?
How can I intercept the close event by adding the less possible markup on the final usage ?
I think it makes sense to use a scoped slot to do this. But you can also try this kind of solution.
Input.vue
<template>
<input
ref="input"
:value="value"
#input="input"
#blur="input"
#keyup.escape="close"
#keyup.enter="input"
/>
</template>
<script>
export default {
props: ['value'],
methods: {
input() {
this.$emit("input", this.$refs.text.value);
this.close();
},
close() {
this.$parent.$emit('close-toggle')
},
}
}
</script>
ToggleWrapper.vue
<template>
<div #click="open = true">
Click
<div v-if="open">
<slot></slot> <!-- Attempt to intercept the close event -->
</div>
</div>
</template>
<script>
export default {
data() {
return {
open: false,
}
},
created() {
this.$on('close-toggle', function () {
this.open = false
})
}
}
</script>
In a Vue3 style, I would use provide and inject (dependency injection). This solution leaves the final markup very light and you still have a lot of control, see it below :
Final usage :
<script setup>
import { ref } from 'vue'
import ToggleWrapper from './ToggleWrapper.vue'
import Input from './Input.vue'
const myText = ref('hi')
const updateMyText = ($event) => {
myText.value = $event
}
</script>
<template>
<ToggleWrapper>
<Input :value="myText" #submit="updateMyText" />
</ToggleWrapper>
<p>value : {{myText}}</p>
</template>
ToggleWrapper.vue
<template>
<div #click="open = true">
<div v-if="open">
<slot></slot>
</div>
<span v-else>Open</span>
</div>
</template>
<script setup>
import { provide, inject, ref } from 'vue'
const open = ref(false)
provide('methods', {
close: () => open.value = false
})
</script>
Input.vue
<template>
<input
:value="value"
#input="input"
#blur="close"
#keyup.escape="close"
#keyup.enter="submit"
/>
</template>
<script setup>
import { inject, ref } from 'vue'
const props = defineProps(['value'])
const emit = defineEmits(['close', 'input', 'submit'])
const methods = inject('methods')
const value = ref(props.value)
const input = ($event) => {
value.value = $event.target.value
emit("input", $event.target.value);
}
const close = () => {
methods.close()
emit('close')
}
const submit = () => {
emit('submit', value.value)
close()
}
</script>
See it working here

vue 3 composition API, conditional rendering

I have a button that needs to change it's icon when clicked in order to toggle between play and pause.
Here is a minimalistic sample of the code :
<template>
<div #click="toggleF">
<i v-if="toggleForce == true" class="fas fa-pause"></i>
<i v-if="toggleForce == false" class="fas fa-play"></i>
</div>
</template>
<script>
import {onMounted, onBeforeMount, ref} from 'vue'
export default {
setup(){
const toggleForce = ref(false)
function toggleF () {
toggleForce.value = !toggleForce.value
};
return {toggleF,toggleForce}
}
}
</script>
I get the following warning/error :
please try the code below:
demo 1
<template>
<div #click="toggleForce = !toggleForce">
<i v-if="toggleForce" class="fas fa-pause" />
<i v-else class="fas fa-play" />
</div>
</template>
<script setup>
import {ref} from 'vue'
const toggleForce = ref(false)
</script>
the <script setup> tag is available since the vue version 3.2 and will help you to clean up your script area
demo 2
const { ref, createApp } = Vue
createApp({
setup() {
const toggleForce = ref(false)
const toggleF = () => {
toggleForce.value = !toggleForce.value
}
return {
toggleForce,
toggleF
}
}
}).mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<button #click="toggleF">toggleForce
<div v-if="toggleForce == true" class="fas fa-pause">TRUE</div>
<div v-if="toggleForce == false" class="fas fa-play">FALSE</div>
</button>
</div>

Vue.js Passing a variable in a loop to another component

As a beginner in Vue.js,I am trying to create a Todo app, but problems seems to passing a variable to another component when looping.
i want to pass item to another embedded component.
Here what I have with all the components:
in main.js:
import Vue from "vue";
import App from "./App.vue";
import Todo from "./components/Todo";
import itemComponent from "./components/itemComponent";
Vue.config.productionTip = false;
Vue.component('todo', Todo);
Vue.component('itemcomponent', itemComponent);
new Vue({
render: h => h(App)
}).$mount("#app");
in App.vue:
<template>
<div id="app">
<todo></todo>
<img alt="Vue logo" src="./assets/logo.png" width="25%" />
<!-- <HelloWorld msg="Hello Vue in CodeSandbox!" /> -->
</div>
</template>
<script>
export default {
name: "App",
components: {
},
};
</script>
in Todo.vue:
<template>
<div>
<input type="text" v-model="nameme" />
<button type="click" #click="assignName">Submit</button>
<div v-for="(item, index) in result" :key="item.id">
<itemcomponent {{item}}></itemcomponent>
</div>
</div>
</template>
<script>
import { itemcomponent } from "./itemComponent";
export default {
props:['item'],
data() {
return {
nameme: "",
upernameme: "",
result: [],
components: {
itemcomponent,
},
};
},
methods: {
assignName: function () {
this.upernameme = this.nameme.toUpperCase();
this.result.push(this.nameme);
this.nameme = "";
},
},
};
</script>
in itemComponent.vue:
<template>
<div>
<input type="text" value={{item }}/>
</div>
</template>
<script>
export default {
props: {
item: String,
},
data() {
return {};
},
};
</script>
what is my mistake? thanks for help.
Quite a bunch of mistakes:
You should import single page components inside their parent components and register them there, not in main.js file. EDIT: Explaination to this is given in docs
Global registration often isn’t ideal. For example, if you’re using a build system like Webpack, globally registering all components means that even if you stop using a component, it could still be included in your final build. This unnecessarily increases the amount of JavaScript your users have to download.
You have a component registration in Todo.vue, but you have misplaced it inside data so its is just a data object that is not getting used, move into components.
In your loop you have item.id, but your item is just a plain string, it does not have an id property. Either change item to object with id property, or simply use the index provided in loop (not recommended).
Pass your item as a prop, not mustache template. So in Todo.vue replace {{ item }} with :item="item"
In your ItemComponent.vue you have mustache syntax once again in the attribute. Change value={{item }} to :value="item"
So here's the final code:
main.js
import Vue from "vue";
import App from "./App.vue";
Vue.config.productionTip = false;
new Vue({
render: h => h(App)
}).$mount("#app");
in App.vue:
<template>
<div id="app">
<todo></todo>
<img alt="Vue logo" src="./assets/logo.png" width="25%" />
<!-- <HelloWorld msg="Hello Vue in CodeSandbox!" /> -->
</div>
</template>
<script>
import Todo from './components/Todo.vue';
export default {
name: "App",
components: {
Todo,
},
};
</script>
in Todo.vue:
<template>
<div>
<input type="text" v-model="nameme" />
<button type="click" #click="assignName">Submit</button>
<div v-for="(item, index) in result" :key="index">
<itemcomponent :item="item"></itemcomponent>
</div>
</div>
</template>
<script>
import { itemcomponent } from "./itemComponent";
export default {
props:['item'],
data() {
return {
nameme: "",
upernameme: "",
result: [],
};
},
methods: {
assignName: function () {
this.upernameme = this.nameme.toUpperCase();
this.result.push(this.nameme);
this.nameme = "";
},
},
components: {
itemComponent,
}
};
</script>
itemComponent.vue:
<template>
<div>
<input type="text" :value="item"/>
</div>
</template>
<script>
export default {
props: {
item: String,
},
data() {
return {};
},
};
</script>

Vue. Change the contents of one slot from another

I have a component with certain slots.
<v-split-container ref="splitContainer" class="panel-panel">
<template slot="left">
Test
</template>
<template slot="right">
<router-view></router-view> //Show Compontent A
</template>
<template slot="bottom"> //Slot B
Hello
</template>
</v-split-container>
Can I, from component A, change the contents of slot B by calling a function inside the component?
Hi you can do it with Scoped Slot. I have created an example for you how to do it. Please not that I am using v-slots(Some context here) only usable from vue v2.6.
Please take a look at the code example: https://codesandbox.io/s/2030jo20j0?fontsize=14
Child Component
<template>
<div>
<div>
<slot name="msgSlot">{{msg}}</slot>
</div>
<br>
<slot name="bottom" :updateMsg="updateMsg"></slot>
</div>
</template>
<script>
export default {
name: "HelloWorld",
data: () => ({
msg: "Default Message"
}),
methods: {
updateMsg(text) {
this.msg = text;
}
}
};
</script>
Parent Component
<template>
<div id="app">
<HelloWorld>
<template v-slot:msgSlot></template>
<template v-slot:bottom="{updateMsg}">
<input type="text" v-model="msg">
<button #click="updateMsg(msg)">Change Msg</button>
</template>
</HelloWorld>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
data: () => ({
msg: ""
}),
components: {
HelloWorld
}
};
</script>