Vue: Get data from parent in <slot> - vue.js

I don't know if what I want is possible in the way I want to do it. I'm using this Vue plugin for modals: https://www.npmjs.com/package/vue-js-modal
I have a component called ModalButton that is responsible for triggering the modal:
<modal-button :modal-data="{{ collect(['account' => $account]) }}" modal-name="leave-account">Leave Account</modal-button>
As you can see, I'm sending :modal-data as props.
The ModalButton component is the following:
<template>
<button #click.prevent="show" type="button" class="px-3 py-2 bg-gray-200 font-bold text-gray-700 ">
<slot></slot>
</button>
</template>
<script>
// # is an alias to /src
export default {
name: 'ModalButton',
data() {
return {
}
},
props: {
modalName: String,
modalData: Object
},
methods: {
show () {
this.$modal.show(this.modalName, this.modalData);
},
hide () {
this.$modal.hide(this.modalName);
}
}
}
</script>
You can see on the show method which comes from the plugin is sending that data to the modal
this.$modal.show(this.modalName, this.modalData);
In my blade template (i'm using laravel), I have the <modal>:
<modal name="leave-account" class="text-center">
<div class="flex items-center justify-center h-full p-6">
<div>
<div>
<div class="text-3xl font-bold mb-2">Are you sure?</div>
<p>If you leave this account, you won't be able to join back unless invited again. </p>
</div>
<footer class="flex items-center justify-end mb-24 mt-auto">
<button class="px-3 py-2 bg-gray-200 font-bold text-gray-700 mr-3">Cancel</button>
<ajax-button
class-list="px-3 py-2 bg-primary text-white font-bold"
:route=#{{SOMEHOW ACCESS MODAL DATA}}
method="delete">
Yes, leave account
</ajax-button>
</footer>
</div>
</div>
</modal>
Inside my modal, I have another component called AjaxButton which allows me to have a reusable button that handles ajax requests.
I need a way of accessing the modal data that was passed to the modal. I'd like not to have to create an additional specific modal component to handle this.
Is this possible?
Specifically what the :route prop needs to send is the Account since my route requires model binding. i.e it'll look something like /accounts/leave/53
Edit:
So I can do this:
<button #click="$modal.hide('leave-account')" class="px-3 py-2 bg-gray-200 font-bold text-gray-700 mr-3">Cancel</button>
(that's right above my <ajax-button>)
So clearly I'm able to access the $modal properties / methods. So there has to be a way to accomplish what I want

You can use a simpler modal:
<modal-button v-model="showModal">
Leave Account
</modal-button>
<modal v-model="showModal" caption="Leave Account">
{{ collect(['account' => $account]) }}
</modal>
Then modal-button will look like this:
<template>
<button #click.stop="$emit('input',true)" type="button" class="px-3 py-2 bg-gray-200 font-bold text-gray-700 ">
<slot/>
</button>
</template>
<script>
export default
{
name: 'ModalButton',
props:
{
value:
{
type: Boolean,
default: false
}
}
}
</script>
and the modal component will be:
<template>
<transition name="modal">
<div class="modal_overlay" #click="close" v-if="value">
<div class="modal_content" style="overflow: auto;" #click.stop>
<div class="modal_title">{{ caption }} <div class="modal_close" #click="close">✖</div></div>
<slot/>
</div>
</div>
</transition>
</template>
<script>
export default
{
props:
{
value:
{
type: Boolean,
default: false
},
caption:
{
type: String,
default: ''
},
created()
{
document.addEventListener('keydown', this.modal_escape, false);
},
beforeDestroy()
{
document.removeEventListener('keydown', this.modal_escape, false);
},
methods:
{
close()
{
this.$emit('input', false);
},
modal_escape(event)
{
var e = event || window.event;
if(this.value && e.keyCode == 27)
{
this.close();
// All good browsers…
if (e.preventDefault) e.preventDefault();
// …and IE
if (e.returnValue) e.returnValue = false;
return false;
}
}
}
}
</script>
<style>
.modal_overlay
{
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 25;
text-align: initial;
background-color: rgba(0,0,0,0.75);
}
.modal_content
{
position: relative;
display: inline-block;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
background-color: white;
border: 2px solid #000;
border-radius: 5px;
max-width: 90vw;
max-height: 90vh;
}
.modal_title
{
text-align: center;
background-color: #F27935;
color: white;
line-height: 1.65;
padding-left: 6px;
}
.modal_close
{
float: right;
display: inline-block;
cursor: pointer;
padding: 0 5px 0 4px;
}
.modal_close:hover
{
color: white;
background-color: #00B4FF;
}
.modal-enter-active
{
transition: all .4s ease;
}
.modal-leave-active
{
transition: all .2s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.modal-enter, .modal-leave-to
{
transform: translateX(10px);
opacity: 0;
}
</style>

Related

Unwanted animation bug in Vue.js

This is an unwanted animation bug that seems to be normal, but I want the element to be animationed which is in <input v-model="todo"/>. The problem is each time I press on add button to add the todo name (<input v-model="todo"/>), the last element 'last' runs the animation.
P.s. I tried to keep the code as simple as possible.
Vue.createApp({
data() {
return {
todo: "",
todo_list: ['last'],
};
},
methods: {
add() {
this.todo_list.unshift(this.todo);
console.log(this.todo_list);
},
remove(index) {
this.todo_list.splice(index, 1);
},
},
}).mount('#app');
.list-enter-active,
.list-leave-active {
transition: all 200ms ease-out;
}
.list-enter-from {
transform: translateY(-20px);
}
.list-leave-to {
opacity: 0;
transform: translateX(20px);
}
article {
width: 50%;
padding: 10px;
background-color: #dddddd;
border: 1px solid #cfcfcf;
border-radius: 5px;
margin-block: 10px
}
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<input type="text" v-model="todo" /> <button #click="add">add</button>
<transition-group name="list" tag="section">
<article v-for="(todo, index) in todo_list" :key="index" #click="remove(index)">
<span>{{ todo }}</span>
</article>
</transition-group>
</div>
The reason you're experiencing this issue, is because you're using the index as a key.
If you added the elements to the end of the list, this would be fine.
But since you're adding it to the start, it will cause the issue you're seeing.
I'd suggest you make each todo an object, and add a unique identifier to each object. This can be a simple integer that you increment. Then you can use that property as the key.
Example
let id = 1;
Vue.createApp({
data() {
return {
todo: "",
todo_list: [{
id: id++,
value: 'last'
}],
};
},
methods: {
add() {
const todo = {
id: id++,
value: this.todo
}
this.todo_list.unshift(todo);
},
remove(index) {
this.todo_list.splice(index, 1);
},
},
}).mount('#app');
.list-enter-active,
.list-leave-active {
transition: all 200ms ease-out;
}
.list-enter-from {
transform: translateY(-20px);
}
.list-leave-to {
opacity: 0;
transform: translateX(20px);
}
article {
width: 50%;
padding: 10px;
background-color: #dddddd;
border: 1px solid #cfcfcf;
border-radius: 5px;
margin-block: 10px
}
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<input type="text" v-model="todo" /> <button #click="add">add</button>
<transition-group name="list" tag="section">
<article v-for="(todo, index) in todo_list" :key="todo.id" #click="remove(index)">
<span>{{ todo.value }}</span>
</article>
</transition-group>
</div>

How can one access the state of other instances of a reusable component in Vue.js

Being new to Vue, came across the following use-case. I have an accordion component that is reused across the application. The accordion simply hides and reveals anything wrapped inside it.
How can I enable closing any other open instance when I open an instance? e.g if prototype accordion is open, then functions and objects accordion is clicked, prototype accordion should close.
const vue = Vue.createApp({});
vue.component('accordion', {
data() {
return {
open: false,
}
},
methods: {
toggle() {
this.open = !this.open;
},
},
template: `
<div class="accord">
<p #click="toggle" class="accord-header">Click me to open and close!</p>
<div v-if="open" class="accord-body">
<slot></slot>
</div>
</div>
`,
})
vue.mount('#main-app');
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
#main-app {
width: 50%;
margin: 0 auto;
}
.accord {
padding: 4px;
cursor: pointer;
}
.accord-header {
text-align: center;
padding: 4px 0;
}
.accord-body {
background-color: #eee;
padding: 8px;
}
<script src="https://unpkg.com/vue#next"></script>
<div id="main-app">
<accordion>
<p class="accord-text">Javascript Proptotypes and inheritance</p>
</accordion>
<accordion>
<p class="accord-text">Scopes and Closures</p>
</accordion>
<accordion>
<p class="accord-text">Functions and Objects</p>
</accordion>
</div>
You can change open to be a prop, then you can do something like activeAccordianId: 1 in the parent and have the prop be
<accordian :open="activeAccordianId === 1" />
<accordian :open="activeAccordianId === 2" />
...

How to handle transitions and animations in Gridsome

Currently i have been using this code in the inline CDN version of VueJS and it works fine, but when i add this to gridsome the click event on the animation to open the slide out doesn't work.
I have looked up that animations for page enter is working but i am not sure how to handle component animations.
Are animations working in Gridsome?
<template>
<nav class="navbar fixed-top navbar-expand-lg navbar-light bg-light">
<div class="row">
<div class="col">
<button
#click="show = !show"
class="navbar-toggler"
type="button"
>
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="col">
<a class="navbar-brand" href="/">Navbar</a>
</div>
</div>
<div>
<transition name="slidein">
<div v-if="show" class="nav-bar">
<button
#click="show = !show"
class="btn-close pl-4 p-3"
type="button"
></button>
<span class="close-btn">X</span>
<div class="d-flex flex-column"></div>
</div>
</transition>
<transition name="fadein">
<div v-if="show = show" class="bck-close" #click="show = !show"></div>
</transition>
</div>
</nav>
</template>
<script>
export default {
name: "headNav",
data: {
show: false
},
};
</script>
<style>
.nav-bar {
height: 100vh;
width: 80vw;
background: #fff;
position: absolute;
left: 0;
z-index: 10000;
overflow-x: hidden;
overflow-y: auto;
top: 0;
}
.btn-close {
background: none;
border: none;
font-size: 1em;
}
.bck-close {
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 1);
position: fixed;
z-index: 9999;
left: 0;
top: 0;
opacity: 0.1;
cursor: pointer;
}
.slidein-enter-active {
animation: slidein 0.2s;
}
.slidein-leave-active {
animation: slidein 0.2s reverse;
}
#keyframes slidein {
from {
left: -100%;
width: 0%;
}
to {
left: 0;
width: 80vw;
}
}
.fadein-enter-active {
animation: fadein 0.2s;
}
.fadein-leave-active {
animation: fadein 0.2s reverse;
}
#keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 0.1;
}
}
</style>
Got the data part wrong, animations do work!
<script>
export default {
name: "headNav",
data () {
return {
show: false
}
},
};
</script>

#click on parent div not firing when pressing child button

I have made a custom number input box component and want it to activate on click. The component consists of 3 elements, two buttons (for decreasing and increasing the number value), and an input, where the number is displayed and can manually be changed. The problem is that the #click of the parent div (.numberField) only fires when clicking the input box, not when the buttons are clicked.
Because the input box seemed to be working I have tried changing the button elements to input[type=button] elements, but that failed.
I have checked when #click of the child elements (the two buttons and the input) fire, and confirmed that all of them behave in the same way (they don't fire on the initial click that sets :disabled="false" on each of them)
My Vue version is 3.7.0 if that matters
<template>
<div class="numberField" #click="fieldClicked">
<button #click="stepDown()" :disabled="disabled" class="left">
−
</button>
<input
v-model="value"
type="number"
:max="max"
:min="min"
:step="step"
:disabled="disabled">
<button #click="stepUp()" :disabled="disabled" class="right">
&plus;
</button>
</div>
</template>
<script>
export default {
name: 'NumberField',
props: {
defaultValue: Number,
max: Number,
min: Number,
step: Number,
disabled: Boolean,
},
data() {
return {
value: this.defaultValue,
};
},
watch: {
value() {
this.value = Math.min(this.max, Math.max(this.min, this.value));
},
},
methods: {
stepDown() {
this.value -= this.step;
},
stepUp() {
this.value += this.step;
},
fieldClicked(event) {
// #click.stop will stop all clicks from propagating; we will only stop clicks when the field is active
if (!this.disabled) event.stopPropagation()
},
},
};
</script>
<style scoped>
.numberField {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 3em;
width: 8em;
}
.left {
border-top-left-radius: 0.2em;
border-bottom-left-radius: 0.2em;
}
.right {
border-top-right-radius: 0.2em;
border-bottom-right-radius: 0.2em;
}
input, button {
padding: 0;
border-radius: 0;
min-width: 0;
height: 100%;
width: 33%;
min-height: 0;
font-size: 1rem;
text-align: center;
}
</style>
This is a summary of how I'm using this component:
<div class="item" v-for="item in items" :key="item.id" #click="toggleItem(item.id)" :class="{ active: activeItems[item.id] }">
<h1>{{ item.name }}, some other elements irrelevant are here too</h1>
<NumberField
:defaultValue="item.amount"
:max="item.amount"
:min="1"
:step="1"
:disabled="!activeItems[item.id]"></NumberField>
</div>
toggleItem(id) toggles the boolean value of activeItems[item.id]. The NumberField is disabled when the item is inactive.
My expectation would be that clicking on any of the child elements of .numberField would fire the #click of .numberField, which (only if the item is inactive) then gets propagated to the #click of .item, but this only seems to be the case when clicking the input[type=number].
I would appreciate any help, I'm absolutely lost!
A <button> with a disabled attribute set will not fire click events. If the event doesn't fire on the <button> then it won't propagate to the <div> either.
In your case a simple workaround would be to put pointer-events: none on your buttons so that the button is skipped altogether. The <div> will just receive the click directly, as though the button wasn't even there.
const NumberField = {
name: 'NumberField',
template: `
<div class="numberField" #click="fieldClicked">
<button #click="stepDown()" :disabled="disabled" class="left">
−
</button>
<input
v-model="value"
type="number"
:max="max"
:min="min"
:step="step"
:disabled="disabled">
<button #click="stepUp()" :disabled="disabled" class="right">
&plus;
</button>
</div>
`,
props: {
defaultValue: Number,
max: Number,
min: Number,
step: Number,
disabled: Boolean,
},
data() {
return {
value: this.defaultValue,
};
},
watch: {
value() {
this.value = Math.min(this.max, Math.max(this.min, this.value));
},
},
methods: {
stepDown() {
this.value -= this.step;
},
stepUp() {
this.value += this.step;
},
fieldClicked(event) {
console.log('here')
// #click.stop will stop all clicks from propagating; we will only stop clicks when the field is active
if (!this.disabled) event.stopPropagation()
},
},
};
new Vue({
el: '#app',
components: {
NumberField
}
})
.numberField {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
height: 3em;
width: 8em;
}
.left {
border-top-left-radius: 0.2em;
border-bottom-left-radius: 0.2em;
}
.right {
border-top-right-radius: 0.2em;
border-bottom-right-radius: 0.2em;
}
input, button {
padding: 0;
border-radius: 0;
min-width: 0;
height: 100%;
width: 33%;
min-height: 0;
font-size: 1rem;
text-align: center;
}
button[disabled] {
pointer-events: none;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<number-field :defaultValue="7" :step="1" :min="0" :max="10" disabled></number-field>
</div>

Vue Accordion with transition

I'm trying to integrate the Accordion component with a body transition, but without success :( . All is working as well except the animation.
template:
<div class="accordion">
<div class="accordion-title" #click="isOpen = !isOpen" :class="{'is-open': isOpen}">
<span>{{title}}</span>
<i class="ic ic-next"></i>
</div>
<div class="accordion-body" :class="{'is-open': isOpen}">
<div class="card">
<slot name="body"></slot>
</div>
</div>
</div>
component:
props: {
title: {
type: String,
default: 'Title'
}
},
data() {
return {
isOpen: false
}
}
And styles:
.accordion-body {
font-size: 1.3rem;
padding: 0 16px;
transition: .3s cubic-bezier(.25,.8,.5,1);
&:not(.is-open) {
display: none;
height: 0;
overflow: hidden;
}
&.is-open {
height: auto;
// display: block;
padding: 16px;
}
}
.card {
height: auto;
}
I tried to use <transition> but it doesn't work with height or display properties.
Help please!
display:none will remove your content and avoid the animation, you should trick with opacity, overflow:hidden and height, but you ll be forced to do a method for that.
For example (not tested, but inspiring):
in template:
<div class="accordion" #click="switchAccordion" :class="{'is-open': isOpen}">
<div class="accordion-title">
<span>{{title}}</span>
<i class="ic ic-next"></i>
</div>
<div class="accordion-body">
<p></p>
</div>
</div>
in component (add a method):
methods: {
switchAccordion: function (event) {
let el = event.target
this.isOpen = !this.isOpen // switch data isOpen
if(this.isOpen) {
let childEl1 = el.childNodes[1]
el.style.height = childEl1.style.height
} else {
let childEl2 = el.childNodes[2]
el.style.height = childE2.style.height // or .clientHeight + "px"
}
}
}
in style:
.accordion {
transition: all .3s cubic-bezier(.25,.8,.5,1);
}
.accordion-body {
font-size: 1.3rem;
padding: 0 16px;
opacity:0
}
.is-open .accordion-body {
opacity:0
}
In this case, your transition should work as you want.
The javascript will change the height value and transition transition: all .3s cubic-bezier(.25,.8,.5,1); will do the animation