I am building a Vue app. I have two components that are related to each other. One of them is called loginRegister.vue and the code of that is here:
loginRegister.vue:
<template>
<BaseModal idBtn = "regis" btnOneText="Cancel" :btnTwoText="button2Text['register']" v-show="showModal1" #close="showModal1 = false" #submitForm="registerUser(regLogPara['register'])" :popoverVue = "showToolTip.register">
<!-- the material that must be shown in modal comes here. it is better to use bootstrap "card" classes for compatible design -->
<!-- ################ -->
<!-- transition part -->
<!-- ################ -->
<transition name="fade">
<div v-if="modalAlert==='form'" key="item1">
<h4 class="card-title text-center my-3">Create Your Account</h4>
</div>
<!-- showing success message -->
<div v-else-if="modalAlert==='success'" key="item2">
<p>you succeed</p>
</div>
<!-- showing error message -->
<div v-else key="item3">
<p>there is an error!</p>
</div>
</transition>
</BaseModal>
</template>
<script>
import BaseButton from './BaseButton.vue';
import BaseInput from './BaseInput.vue'
import BaseModal from './BaseModal.vue';
import { onMounted, reactive, ref } from 'vue';
export default {
name: "loginRegister",
components: {
BaseModal,
BaseButton,
BaseInput
},
setup(props) {
const showModal = ref(false);
const showModal1 = ref(false);
const modalAlert = ref("form");
const registerUser = async (checkPara) => {
/* This function is responsible for sending form data to backend and getting the result from backend */
if (checkPara == "reset") {
/* for showing form again if there is a back-end error */
console.log("reset form");
modalAlert.value = "form";
button2Text["login"] = "Submit";
button2Text["register"] = "Submit";
regLogPara["login"] = "login";
regLogPara["register"] = "register";
} else {
/* for submiting form */
modalAlert.value = "error";
button2Text[checkPara] = "Try again";
regLogPara[checkPara] = "reset";
console.log("register function");
}
}
return {
registerUser,
showModal,
showModal1,
validation,
blurInput,
loginValid,
registerValid,
finalCheck,
showToolTip,
store,
modalAlert,
button2Text,
regLogPara
}
}
};
</script>
The other one called BaseModal.vue and the code of that is here:
BaseModal.vue:
<template>
<div class="modal-overlay container-fluid p-0">
<div class="row align-items-center justify-content-center" #click.self="$emit('close')">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<!-- text and other html comes here in the slot -->
<!-- ############## -->
<!-- this is the slot part -->
<!-- ############## -->
<slot></slot>
<div class="card-footer d-flex justify-content-around align-items-center">
<base-button
large
v-if="btnOneText"
kind = "btn-secondary"
:textBtn = "btnOneText"
#click="$emit('close')"
>
</base-button>
<base-button
large
v-if="btnTwoText"
kind = "btn-secondary"
:textBtn = "btnTwoText"
#click="popoverFunc"
#blur="popoverDisable"
:id="idBtn"
>
</base-button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import BaseButton from './BaseButton.vue';
import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min.js";
export default {
// $emit('submitForm'),
name: "BaseModal",
props: {
btnOneText: {
type: String
},
btnTwoText: {
type: String
},
popoverVue: {
type: Boolean
},
idBtn: {
type: String
}
},
computed: {
popOverData: function () {
return this.popoverVue;
},
popVar: function() {
return new bootstrap.Popover(document.getElementById(this.idBtn), {
trigger: "manual",
title: "Notice:",
content: "Please fill all fields in the correct way to submit your information!",
customClass: "myPopover",
placement: "top"
})
}
},
data() {
return {
showPopOver: false
}
},
emits: ["close", "submitForm"],
components: {
BaseButton
},
watch: {
popOverData: function (newState, oldState) {
if(newState === true) {
this.showPopOver = true;
this.popoverFinal();
} else {
this.showPopOver = false;
this.popoverFinal();
}
}
},
methods: {
popoverFunc: function() {
this.$emit('submitForm');
if (this.popOverData) {
this.showPopOver = true;
this.popoverFinal();
}
},
popoverDisable: function() {
console.log("popoverDisable");
this.showPopOver = false;
this.popoverFinal();
},
popoverFinal: function() {
console.log(this.showPopOver);
/* this function is responsible for showing and hiding the popover according to "showPopOver" data */
if (this.showPopOver) {
this.popVar.show();
} else {
this.popVar.hide();
}
}
}
};
</script>
<style scoped src="../assets/css/compoStyles/baseModal.css"></style>
There are some codes that are not related to this question and also I tried to simplify the codes and clarify the parts that are related to transition and slot in my components code.
Although the codes may seem long or complicated, the goal that I want to reach is simple. I want to submit a register form in a modal component. Actually the user in my app clicks on register button and then the BaseModal.vue component is shown. In that case the register form (that for simplicity I substitute that with a h4 tag) is the default thing that user could see. After submitting the form according that the process is successful or there is an error, I want to show a message to the user and change the text of button from submit to try again if there is an error. After that when the user clicks try again button the form (h4 tag) must be fade in again. So I tried the v-if/v-else-if/v-else structure of Vue in my loginRegister.vue component. The code that I used in transition part is similar to the code that Vue documentation is used, But the transition does not work correctly. In my local development environment, the h4 tag disappears smoothly and then no message is shown, after clicking try again the h4 tag fade in again. Also in the console I could see this warning:
[Vue warn]: <transition> can only be used on a single element or component. Use <transition-group> for lists.
at <BaseTransition mode=undefined appear=false persisted=false ... >
at <Transition name="fade" >
at <BaseModal idBtn="regis" btnOneText="Cancel" btnTwoText="Try again" ... >
at <LoginRegister>
at <App>
But I don't think that is related to my issue, because I did not use transition on multi elements in my app. So could anyone please help me that what is wrong in my codes?
It seems like you want baseModal.vue to be inside loginRegister.vue and the baseModal to have some kind of transition whenever some state changes? Try changing <transition></transition> into <transition-group></transition-group>
Related
I have in Layout.vue to components one TheSidebar second TheHeader, there is a button in TheHeader to open the sidebar in TheSidebarcomponent.
I need to when I click the button in header open the sidebar:
My try:
in TheHeader:
methods: {
openSidebar() {
this.$root.$emit("open-sidebar");
},
},
in TheSidebar
data() {
return {
sidebarOpen: false,
};
},
mounted() {
this.$root.$on("open-sidebar", (this.sidebarOpen = true));
},
I'm using VUE 3 so I got this error in console: TypeError: this.$root.$on is not a function so How can communicate ?
you can use something like tiny emitter it works fine and doesn't care about parent child relationship
var emitter = require('tiny-emitter/instance');
emitter.on('open-sidebar', ({isOpen}) => {
//
});
emitter.emit('open-sidebar', {isOpen : true} );
You can only pass props to a direct child component, and
you can only emit an event to a direct parent. But
you can provide and eject from anywhere to anywhere
Per another answer, provide and eject may be your best bet in Vue 3, but I created a simple example of how to implement with props/events. Built with Vue 2 as I haven't worked with 3 yet, but should be usable in Vue 3 as well.
Parent.vue
<template>
<div class="parent">
<div class="row">
<div class="col-md-6">
<h4>Parent</h4>
<hr>
<child-one #show-child-two-event="handleShowChildTwoEvent" />
<hr>
<child-two v-if="showChildTwo" />
</div>
</div>
</div>
</template>
<script>
import ChildOne from './ChildOne.vue'
import ChildTwo from './ChildTwo.vue'
export default {
components: {
ChildOne,
ChildTwo
},
data() {
return {
showChildTwo: false
}
},
methods: {
handleShowChildTwoEvent() {
this.showChildTwo = true;
}
}
}
</script>
ChildOne.vue
<template>
<div class="child-one">
<h4>Child One</h4>
<button class="btn btn-secondary" #click="showChildTwo">Show Child Two</button>
</div>
</template>
<script>
export default {
methods: {
showChildTwo() {
this.$emit('show-child-two-event');
}
}
}
</script>
ChildTwo.vue
<template>
<div class="child-two">
<h4>Child Two</h4>
</div>
</template>
I have an vue component which uses Vue Draggable .
<template>
<div class="row my-5">
<div v-for="column in columns" :key="column.title" class="col">
<p class="font-weight-bold text-uppercase">{{column.title}}</p>
<!-- Draggable component comes from vuedraggable. It provides drag & drop functionality -->
<draggable :list="column.tasks" :animation="200" ghost-class="ghost-card" group="tasks" :move="checkMove">
<transition-group>
<task-card
v-for="(task) in column.tasks"
:key="task.id"
:task="task"
class="mt-3 cursor-move"
></task-card>
<!-- </transition-group> -->
</transition-group>
</draggable>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
import TaskCard from "../board/TaskCard";
export default {
name: "App",
components: {
TaskCard,
draggable,
},
data() {
return {
columns: [
.....
],
};
},
methods: {
checkMove: function(evt){
console.log('moved');
}
},
};
</script>
In TaskCard Component -
<template>
<div class="bg-white shadow rounded p-3 border border-white">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>{{task.id}}</h2>
<span>{{task.date}}</span>
</div>
<p class="font-weight-bold">{{task.title}}</p>
</div>
</template>
<script>
export default {
props: {
task: {
type: Object,
default: () => ({}),
},
},
};
</script>
When I move an item, I want a modal that confirms the change and only then move the item.
(ie. if I click on the cancel button inside the modal, the item should not be moved.)
How can this be achieved using the checkMove() function provided?
I don't think you can achieve this by using onMove event. The onEnd event it seems more suitable but unfortunately it doesn't have any cancel drop functionality.
So I think the only solution here is revert it back if the user decides to cancel.
You can listen on change event (See more in documentation)
<draggable
group="tasks"
v-model="column.tasks"
#change="handleChange($event, column.tasks)">
...
</draggable>
...
<button #click="revertChanges">Cancel</button>
<button #click="clearChanges">Yes</button>
And
...
handleChange(event, list) {
this.changes.push({ event, list })
this.modal = true
},
clearChanges() {
this.changes = []
this.modal = false
},
revertChanges() {
this.changes.forEach(({ event, list }) => {
if (event.added) {
let { newIndex } = event.added
list.splice(newIndex, 1)
}
if (event.removed) {
let { oldIndex, element } = event.removed
list.splice(oldIndex, 0, element)
}
if (event.moved) {
let { newIndex, oldIndex, element } = event.moved
list[newIndex] = list[oldIndex]
list[oldIndex] = element
}
})
this.changes = []
this.modal = false
}
...
JSFiddle example
I have am building a Vue app that includes a QuillJS editor in a tab. I have a simple setTab(tabName) Vue method that shows/hides tabs with the v-if directive.
methods: {
setTab: function (tabName) {
this.view = tabName;
if(tabName === 'compose') {
var editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
}
}
}
My tab is basically like this:
<div id="composer" v-if="tabName === 'compose'">
<!-- toolbar container -->
<div id="toolbar">
<button class="ql-bold">Bold</button>
<button class="ql-italic">Italic</button>
</div>
<!-- editor container -->
<div id="editor">
<p>Hello World!</p>
</div>
</div>
Currently, I'm getting an error because the #editor element does not yet exist when I am calling new Quill(...). How do I delay that QuillJS initialization on the page so that it doesn't happen until after the #editor is already there?
Use mounted hook.
mounted: function () {
// Code that will run only after the
// entire view has been rendered
}
Use this.$nextTick() to defer a callback to be executed after the next DOM update cycle (e.g., after changing a data property that causes a render-update).
For example, you could do this:
methods: {
setTab: function (tabName) {
this.view = tabName;
if(tabName === 'compose') {
this.$nextTick(() => {
var editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
})
}
}
}
A clean way to do this is not to rely on selectors but make Quill editor a self-contained component:
<template>
<div class="quill-editor">
<!-- toolbar container -->
<div ref="toolbar">
<button class="ql-bold">Bold</button>
<button class="ql-italic">Italic</button>
</div>
<!-- editor container -->
<div ref="editor">
<p>Hello World!</p>
</div>
</div>
</template>
<script>
...
name: "QuillEditor",
mounted() {
this.quill = new Quill(this.$refs.editor, {
modules: { toolbar: this.$refs.toolbar },
theme: 'snow'
});
}
...
</script>
I want to change the font size of the element using a custom directive
Thus I have tried below code for that
<template>
<div class="hello"><label v-onhover>CLICK ME TO CHANGE FONT</label></div>
</template>
<script>
export default {
name: "CustomDirective",
props: {
msg: String
},
directives: {
onhover: {
bind(el, binding) {
el.onmouseover = function() {
el.fontSize = "100px";
};
}
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
Inside the bind, I'm getting the complete label element but don't know how to get it worked for changing the user-defined font size on mouse hover
You want el.style.fontSize instead of el.fontSize.
Vue.directive('onhover', {
bind(el, binding) {
el.onmouseover = function() {
el.style.fontSize = "100px";
};
}
});
new Vue().$mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="hello">
<label v-onhover>Hover on me to change font</label>
</div>
</div>
After trying a few different ways, I have solved it out the situation
Below is my code for the solution
<template>
<div class="hello">
<label v-onhover>{{ msg }}</label>
</div>
</template>
<script>
export default {
name: "CustomDirective",
data() {
return {
str: "",
msg: "Welcome to Your Vue.js App"
};
},
directives: {
onhover: {
bind(el, binding) {
el.onmouseover = function() {
el.style.fontSize = "100px";
};
el.onmouseout = function() {
el.style.fontSize = "15px";
};
}
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>
Here in the above example, I have used two different event mouseover and mouseout event and based on the event state, we can change the property of an element
How can I use named slots from dynamic components in the parent component?
A slider component takes an array of dynamic slide components:
<slider :slides="slides" />
Each slide has named slots with content to be using by the slider:
<template>
<div class="slide">
<div slot="main">Slide 1 Main</div>
<div slot="meta">Slide 1 Meta</div>
</div>
</template>
The slider should now use these slots, like so:
<template>
<div class="slider">
<div class="slider__slide" v-for="slide in slides">
<component :is="slide">
<div class="slider__slide__main">
<slot name="main" /><!-- show content from child's slot "main" -->
</div>
<div class="slider__slide__meta">
<slot name="meta" /><!-- show content from child's slot "meta" -->
</div>
</component>
</div>
</div>
</template>
But <component> ignores its inner content, so the slots are ignored.
Example:
https://codepen.io/anon/pen/WZjENK?editors=1010
If this isn't possible, is there another way to create a slider that takes HTML content from slide components without caring about their content?
By splitting the main/meta sections into their own components, you can relatively easily use a render function to split them into the sections you want.
console.clear()
const slide1Meta = {
template:`<div>Slide 1 Meta</div>`
}
const slide1Main = {
template: `<div>Slide 1 Main</div>`
}
const slide2Meta = {
template:`<div>Slide 2 Meta</div>`
}
const slide2Main = {
template: `<div>Slide 2 Main</div>`
}
Vue.component('slider', {
props: {
slides: {
type: Array,
required: true
}
},
render(h){
let children = this.slides.map(slide => {
let main = h('div', {class: "slider__slide__main"}, [h(slide.main)])
let meta = h('div', {class: "slider_slide_meta"}, [h(slide.meta)])
return h('div', {class: "slider__slide"}, [main, meta])
})
return h('div', {class: "slider"}, children)
}
});
new Vue({
el: '#app',
data: {
slides: [
{meta: slide1Meta, main: slide1Main},
{meta: slide1Meta, main: slide2Main}
]
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<slider :slides="slides" />
</div>
<script type="text/x-template" id="slide1-template">
<div class="slide">
<div slot="main">Slide 1 Main</div>
<div slot="meta">Slide 1 Meta</div>
</div>
</script>
<script type="text/x-template" id="slide2-template">
<div class="slide">
<div slot="main">Slide 2 Main</div>
<div slot="meta">Slide 2 Meta</div>
</div>
</script>
Actually slots within a dynamic component element do work. I have have been attempting to solve this same issue, and found this lovely little example by Patrick O'Dacre on CodePen. Patrick has made ample and useful comments in his code which I paste verbatim here for posterity. I omitted the css which you can find on CodePen.
const NoData = {
template: `<div>
This component ignores the data completely.
<p>But there are slots!</p>
<slot></slot>
<slot name="namedSlot"></slot>
</div>`
// In this component, I just ignore the props completely
}
const DefaultMessage = {
template: `<div>
This component will show the default msg: <div>{{parentData.msg}}</div>
</div>`,
// this component won't have posts like the Async Component, so we just ignore it
props: ['parentData']
}
const CustomMessage = {
template: `<div>
This component shows a custom msg: <div>{{parentData.msg}}</div>
</div>`,
// this component won't have posts like the Async Component, so we just ignore it
props: ['parentData']
}
const Async = {
template: `<div>
<h2>Posts</h2>
<p>{{parentData.msg}}</p>
<section v-if="parentData.posts.length > 0">
<ul>
<li class="postInfo" v-for="post in parentData.posts">
<div class="postInfo__title">
<strong>Title:</strong> {{post.title}}
</div>
</li>
</ul>
</section>
</div>`,
props: ['parentData']
}
/* Children should only affect parent properties via an EVENT (this.$emit) */
const ChangeMessage = {
template: `<div>
<p>Type here to change the message from the child component via an event.</p>
<div><input type="text" v-model="message" #input="updateDateParentMessage" /></div>
</div>`,
data() {
return {
// initialize our message with the prop from the parent.
message: this.parentData.msg ? this.parentData.msg : ''
}
},
props: ['parentData'],
/* Need to watch parentData.msg if we want to continue
to update this.message when the parent updates the msg */
watch: {
'parentData.msg': function (msg) {
this.message = msg
}
},
methods: {
updateDateParentMessage() {
this.$emit('messageChanged', this.message)
}
}
};
const Home = {
template: `<section>
<div class="wrap">
<div class="right">
<p><strong>Change the current component's message from the Home (parent) component:</strong></p>
<div><input type="text" v-model="dataForChild.msg" /></div>
<p><strong>Important!</strong> We do not change these props from the child components. You must use events for this.</p>
</div>
</div>
<div class="controls">
<button #click="activateComponent('NoData')">No Data</button>
<button #click="activateComponent('DefaultMessage')">DefaultMessage</button>
<button #click="activateComponent('CustomMessage', {posts: [], msg: 'This is component two'})">CustomMessage</button>
<button #click="getPosts">Async First</button>
<button #click="activateComponent('ChangeMessage', {msg: 'This message will be changed'})">Change Msg from Child</button>
<button #click="deactivateComponent">Clear</button>
</div>
<div class="wrap">
<div class="right">
<h2>Current Component - {{currentComponent ? currentComponent : 'None'}}</h2>
<!-- ATTN: Uncomment the keep-alive component to see what happens
when you change the message in ChangeMessage component and toggle
back and forth from another component. -->
<!-- <keep-alive> -->
<component
:is="currentComponent"
:parentData="dataForChild"
v-on:messageChanged="updateMessage">
<div class="slotData">This is a default slot</div>
<div slot="namedSlot" class="namedSlot">This is a NAMED slot</div>
<div slot="namedSlot" class="namedSlot"><p>Here we pass in the message via a slot rather than as a prop:</p>{{dataForChild.msg}}</div>
</component>
<!-- </keep-alive> -->
</div>
</div>
</section>`,
data() {
return {
currentComponent: false,
/* You don't NEED to put msg and posts here, but
I prefer it. It helps me keep track of what info
my dynamic components need. */
dataForChild: {
// All components:
msg: '',
// Async Component only
posts: []
}
}
},
methods: {
/**
* Set the current component and the data it requires
*
* #param {string} component The name of the component
* #param {object} data The data object that will be passed to the child component
*/
activateComponent(component, data = { posts: [], msg: 'This is a default msg.'}) {
this.dataForChild = data;
this.currentComponent = component;
},
deactivateComponent() {
this.dataForChild.msg = '';
this.currentComponent = false;
},
/* Hold off on loading the component until some async data is retrieved */
getPosts() {
axios.get('https://codepen.io/patrickodacre/pen/WOEXOX.js').then( resp => {
const posts = resp.data.slice(0, 10) // get first 10 posts only.
// activate the component ONLY when we have our results
this.activateComponent('Async', {posts, msg: `Here are your posts.`})
})
},
/**
* Update the message from the child
*
* #listens event:messageChanged
* #param {string} newMessage The new message from the child component
*/
updateMessage(newMessage) {
this.dataForChild.msg = newMessage
}
},
// must wire up your child components here
components: {
NoData,
CustomMessage,
DefaultMessage,
Async,
ChangeMessage
}
}
const routes = [
{ path: '/', name: 'home', component: Home}
];
const router = new VueRouter({
routes
});
const app = new Vue({
router
}).$mount("#app")
The html,
<div id="app">
<h1>Vue.js Dynamic Components with Props, Events, Slots and Keep Alive</h1>
<p>Each button loads a different component, dynamically.</p>
<p>In the Home component, you may uncomment the 'keep-alive' component to see how things change with the 'ChangeMessage' component.</p>
<nav class="mainNav"></nav>
<!-- route outlet -->
<!-- component matched by the route will render here -->
<section class="mainBody">
<router-view></router-view>
</section>
</div>