How to extend component and use slot - vue.js

Goal: I want to be able to have a modal template that I can extend in other pages in my Vue.js (Nuxt.js) application
ModalTemplate.vue:
<!-- Base Modal Component -->
<template>
<!-- Modal -->
<div class="modal opacity-0 pointer-events-none fixed w-full h-full top-0 left-0 flex items-center justify-center">
<div class="modal-overlay absolute w-full h-full bg-gray-900 opacity-50"></div>
<!-- Modal Container -->
<div class="modal-container bg-gray-300 w-5/12 mx-auto rounded shadow-lg z-50 overflow-y-auto">
<!-- Top Right escape button (needs to be within the container for z-index purposes) -->
<div class="modal-close absolute top-0 right-0 cursor-pointer flex flex-col items-center mt-4 mr-4 text-white text-sm z-50">
<fa icon="times" class="fa-2x"></fa>
<span class="text-sm">(Esc)</span>
</div>
<div class="modal-content">
<!-- Title of Modal -->
<div class="modal-title-container">
<slot name="modal-header"></slot>
</div>
<!-- Body of Modal -->
<div class="modal-body-container">
<slot></slot>
</div>
<!-- Footer of Modal -->
<div class="modal-footer-container">
<slot name="modal-footer"></slot>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ModalTemplate',
data() {
return {
}
},
methods: {
toggleModal: function() {
var body = document.querySelector('body');
var modal = document.querySelector('.modal');
modal.classList.toggle('opacity-0');
modal.classList.toggle('pointer-events-none');
body.classList.toggle('modal-active');
},
modalHidden: function () {
this.toggleModal();
this.messageBus.$emit('closing')
},
keyDownPressed: function(keyPressed) {
var isEscape = false
if (keyPressed.key === "Escape" || keyPressed.key === "Esc") {
isEscape = true
} else {
isEscape = (keyPressed.keyCode === 27)
}
if (isEscape && document.querySelector('body').classList.contains('modal-active')) {
this.modalHidden();
}
}
},
created: function() {
window.addEventListener('keydown', this.keyDownPressed)
},
destroyed: function() {
window.removeEventListener('keydown', this.keyDownPressed)
},
mounted: function() {
this.toggleModal();
var closeModalSelector = document.querySelectorAll('.modal-close')
for (var i = 0; i < closeModalSelector.length; i++) {
closeModalSelector[i].addEventListener('click', this.modalHidden)
}
const overlay = document.querySelector('.modal-overlay')
overlay.addEventListener('click', this.modalHidden);
}
}
</script>
<style lang="postcss">
.modal-page {
#apply pointer-events-none;
#apply fixed;
#apply w-full;
#apply h-full;
#apply top-0;
#apply left-0;
#apply flex;
#apply items-center;
#apply justify-center;
}
.modal-overlay {
#apply absolute;
#apply w-full;
#apply h-full;
#apply bg-gray-900;
}
.modal-container {
#apply bg-gray-300;
#apply mx-auto;
#apply rounded;
#apply shadow-lg;
#apply z-50;
#apply overflow-y-auto;
}
.modal-content {
#apply py-4;
}
.modal-title-container {
#apply flex;
#apply justify-between;
#apply items-center;
#apply border-b;
#apply border-gray-400;
#apply px-4;
#apply pb-3;
}
.modal-body-container {
#apply py-2;
#apply px-4;
}
.modal-footer-container {
#apply flex;
#apply border-t;
#apply border-gray-400;
#apply px-4;
#apply pt-2;
}
</style>
CertificateDetailsModal.vue:
<template>
<ModalTemplate ref="modal">
<template v-slot:modal-header>
This is a header
</template>
</ModalTemplate>
</template>
<script>
import ModalTemplate from '~/components/Modals/ModalTemplate'
export default {
name: 'DetailsModal',
components: {
ModalTemplate
},
model: {
prop: 'certificate',
event: 'input'
},
props: {
certificate: {
type: Object,
default: null
}
},
mounted() {
},
methods: {
closeModal: function() {
alert('closing modal!')
this.$store.dispatch('certificates/loadCertificates')
this.$emit('input', null);
}
}
}
</script>
<style scoped>
</style>
Attempts:
I looked at extending the modal, but it gave me quite a few errors when I tried to dismiss the modal (I can provide the code if needed).
Question:
How can I extend the Modal (getting all the functionality of the functions) while adding additional functionality in the CertificateDetailsModal (such as functions, methods, and html)?

You could re-declare the slots in your wrapper component's template. For instance, the following template declares a modal-footer slot and a default slot (unnamed assumed to have a name of default):
<!-- CertificateDetailsModal.vue -->
<template>
<ModalTemplate>
<template v-slot:modal-header>
My header
</template>
<template v-slot:modal-footer> <!-- pass `modal-footer` slot to ModalTemplate -->
<slot name="modal-footer"></slot>
</template>
<slot /> <!-- pass `default` slot to ModalTemplate -->
</ModalTemplate>
</template>
Then your app could use the CertificateDetailsModal like this:
<!-- App.vue -->
<template>
<CertificateDetailsModal>
<template v-slot:modal-footer>
<footer>My footer</footer>
</template>
<span>My default</span>
</CertificateDetailsModal>
</template>
demo

Related

How to change styles of child component based on grid / list toggled in parent component, in vue?

I have a view in Vue project.
Home.vue
<template>
<TestLayout >
<Card/>
<Card/>
<Card/>
</TestLayout>
</template>
<script>
import TestLayout from "../components/TestLayout.vue"
import Card from "../components/Card.vue"
export default {
name: "Home",
props:{
isList:{
type: Boolean
}
},
components: {
TestLayout,
Card
},
}
</script>
The TestLayout has a section where we can display cards in list or grid view
TestLayout.vue
<template>
<div class="flex border-solid ">
<ListBulletIcon class="h-10 w-10 cursor-pointer shadow border-2 border-indigo-600 rounded-l p-2"
#click="listView = true" />
<TableCellsIcon #click="listView = false"
class="h-10 w-10 cursor-pointer shadow border-2 border-indigo-600 rounded-r p-2" />
</div>
<section
:class="[listView ? 'md:grid-cols-1 grid-cols-1' : 'md:grid-cols-4 grid-cols-2', 'rounded-md grid gap-5 col-span-full']">
<slot :listView="listView"></slot>
</section>
</template>
<script>
import {
ListBulletIcon,TableCellsIcon} from '#heroicons/vue/24/outline'
export default {
data: function () {
return {
listView: false,
}
},
components: {
ListBulletIcon,
TableCellsIcon,
},
}
}
</script>
I want to change the style of Card.vue based on whether user clicks grid view or list view icon.
For example, I want to add this style to Card.vue div tag in its template:
:class="[isList ? 'dark:bg-midnight' : 'dark:bg-red-300', 'min-h-80 w-full bg-gray-50 shadow-xl rounded-md flex flex-col']"
How will I check isList is clicked or not?
How can I achieve this?
You're half way there. After defining a slot prop (<slot :listView="listView"></slot>) you should access it in parent and pass it down to slot components.
<TestLayout>
<template v-slot="{ listView }">
<Card :isList="listView" />
<Card :isList="listView" />
</template>
</TestLayout>

Use event #change and #click to get data pull-and-drop in vuejs?

Template:
<div class="row">
<div class="col-lg-12 mt-2">
<draggable
class="list-group"
tag="ul"
v-model="imgList.top"
v-bind="dragOptions"
>
<transition-group type="transition">
<li
class="list-group-item" style="align-items: center;display: flex;padding: 12px 15px; font-size: 13px"
v-for="record in imgList.top"
:key="record._id"
#click="onClickValue"
<!-- #change="onChangeValue" -->
>
<div class="col-lg-12">
<img class="w-100" :src="getImageURL(record.image)" alt />
</div>
</li>
</transition-group>
</draggable>
</div>
</div>
<script>
import draggable from "vuedraggable";
export default {
components: {
draggable
},
data() {
return {
imgList: {
top: [
{_id: '12354356444433', image: '09jgg24.jpg'},
{_id: '12354356442211', image: '09jaef2.jpg'},
]
}
}
},
methods: {
onClickValue() {
console.log('ok') // No results received
},
//onChangeValue() {
// console.log('ok') // No results received
//},
}
}
</script>
I am doing drag-and-drop in vuejs. I hold the image and drag and drop it.. Now I want when I drag an image to catch the #click or #change event. I tried putting #click and #change in my code. But when I drag and drop nothing appears? Please show me to catch that event. Thank you

Why does this Vue3 transition break data binding?

I have this issue I've been hitting for hours now; I can't understand why it doesn't work as expected.
I pasted an example code below. The issue is that when editing the name, {{name}} is not updated. However, if I remove either of the <transition> element or the v-if="show" condition, then data binding works as expected. Same if the {{name}} is placed outside the transition.
So it seems the transition blocks data binding? However I don't find anything about it in the docs or elsewere. I tested this code in a Vue2 playground, and it works as expected (data binding works). So the behavior seems to depend on Vue3.
Is there something I'm missing? Is it a bug in Vue3?
Thanks in advance for any input or idea.
<template>
<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<div v-if="show">
<p>hello, {{name}}</p>
<input v-model="name" type="text" />
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
data() {
return {
name: "",
show: true,
}
}
});
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.8s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
It works just fine in plain JS...
So try to focus on the differences:
TypeScript (i cannot use it here on SO) - I really doubt its the cause but you can try
Scoped CSS - did you tried to remove scoped ? There are some issues with scoped CSS and <transition>. Check this issue in Vue-loader. My example is not build with Webpack so Vue-loader is not used but it's for sure used in your project...
const app = Vue.createApp({
data() {
return {
name: "",
show: true,
}
},
template: `
<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<div v-if="show">
<p>hello, {{name}}</p>
<input v-model="name" type="text" />
</div>
</transition>
</div>
`
}).mount("#app");
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.8s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app"></div>
I meet same question, you can try to set the initial value of 'show' to false and at the lifeCycle to modify 'show' for true.

vueJs equivalent of jQuery slideDown/slideUp or slideToggle

I'm experimenting with vue for the first time.
I've replaced a jquery show/hide that was using .slideDown() / .slideUp() with v-show - however I much prefer the animation of jQuery's slideup/down. Is there an easy way to do this with vue?
Simplified code here:
<nav class="bg-blue" role="navigation">
<div class="container classes here">
<div class="classes here">
<h1 class="classes here">
<a href="/" class="classes here">Site Name
</a>
</h1>
<button class="classes here" #click="isShowing ^= true">
hamburger svg here
</button>
</div>
<div class="main-nav classes here" v-show="!isShowing">
<div class="classes here">
<!-- nav items here -->
</div>
</div>
</div><!-- /.container -->
</nav>
Please advise.
You can install modules for this, or you can write a custom component. I suggest the second option.
<template>
<div>
<button #click="isShowing">
hamburger svg here
</button>
<!-- animation of appearance/disappearance.
More info: https://v2.vuejs.org/v2/guide/transitions.html -->
<!-- The attribute "name" is used to create custom classes
that are applied during animation -->
<transition name="main-nav"
#enter="transitionStep1"
#after-enter="transitionStep2"
#before-leave="transitionStep3"
#after-leave="transitionStep4">
<div class="main-nav" v-show="!active">
<div class="classes here">Some text</div>
</div>
</transition>
</div>
</template>
<script>
export default {
name: "Accordion",
data() {
return {
// set the variable that will hide/show the block
active: false
}
},
methods: {
isShowing() {
// change the value of the variable that will hide/show the block
this.active = !this.active;
},
transitionStep1(el) {
// set the block height at the moment of its appearance
el.style.height = el.scrollHeight + 'px'
},
transitionStep2(el) {
// remove inline styles from the block after animation of its appearance
el.style.height = ''
},
transitionStep3(el) {
// set the height of the block at the beginning of its disappearance animation
el.style.height = el.scrollHeight + 'px'
},
transitionStep4(el) {
// remove inline styles from the block after the animation of its disappearance
el.style.height = ''
},
},
}
</script>
<style lang="scss" scoped>
.main-nav {
overflow: hidden;
-webkit-transition: height 0.3s ease;
transition: height 0.3s ease;
}
.main-nav-enter {
height: 0;
}
.main-nav-leave-to {
height: 0 !important;
}
</style>

What is the correct way to import ng2-bootstrap into an Angular2 application?

I am trying to have a responsive application bar based on the bootstrap example here - http://getbootstrap.com/components/#navbar
I removed most of the code and placed the result on JSBin - http://jsbin.com/juvalaj/edit?html,output. When the screen is resized to extra small the right-most options collapse into a menu:
Clicking the right most button yields:
Nice.
Now I am trying to do the same in Angular 2. Here is the plunker - http://plnkr.co/edit/doDyRQSV6aBqZ0KLgGnz
The css works correctly, but clicking the button does nothing - we need the bootstrap code for that. I am puzzled where to plug the ng2-bootstrap. I do not need to import any components from it, but if it works like the original bootstrap then just the essence of including it should make the button work.
The plunker was started off the Angular 2 template (http://plnkr.co/edit/nl0F7N?p=preview) by:
adding app/app-bar.component.ts:
import { Component } from '#angular/core';
#Component({
selector: 'app-bar',
styles: [`
.app-bar {
height: 65px;
padding: 5px 30px;
background-color: #00BCD4;
}
.logo {
color: white;
font-size: 30px;
font-weight: 300;
cursor: pointer;
}
.link {
color: white;
font-size: 24px;
font-weight: 400;
cursor: pointer;
}
`],
template: `
<nav class="app-bar navbar navbar-default">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#app-bar-collapse" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="logo navbar-brand" href="#">My App</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="app-bar-collapse">
<ul class="nav navbar-nav navbar-right">
<li>Settings</li>
<li>Sign out</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
`
})
export class AppBarComponent {}
changing app/app.component.ts to
import { Component } from '#angular/core';
import { AppBarComponent } from './app-bar.component';
#Component({
selector: 'my-app',
directives: [AppBarComponent],
template: '<app-bar></app-bar>'
})
export class AppComponent { }
adding bootstrap css to index.html:
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
clearing completely styles.css
And now I am looking for a way to import ng2-bootstrap code. How can I do it?
Ok, here is a working example: http://embed.plnkr.co/k9s1LnJU5cVy18yid7Va/
You had to:
Configure SystemJS to load ng2-boostrap and moment
//map tells the System loader where to look for things
var map = {
'app': 'app',
.....
'ng2-bootstrap': 'https://npmcdn.com/ng2-bootstrap#1.0.24',
'moment': 'https://npmcdn.com/moment',
};
//packages tells the System loader how to load when no filename and/or no extension
var packages = {
'app': {
main: 'main.ts',
defaultExtension: 'ts'
},
...
'ng2-bootstrap': {
main: 'ng2-bootstrap.js',
defaultExtension: 'js'
},
'moment': {
main: 'moment.js',
defaultExtension: 'js'
}
};
Import the ng2-bootstrap directives
import {BUTTON_DIRECTIVES} from 'ng2-bootstrap/ng2-bootstrap';
And here is your component with a working toggle button:
import { Component } from '#angular/core';
import { AppBarComponent } from './app-bar.component';
import {CORE_DIRECTIVES} from '#angular/common';
import {FORM_DIRECTIVES} from '#angular/forms';
import {BUTTON_DIRECTIVES} from 'ng2-bootstrap/ng2-bootstrap';
#Component({
selector: 'my-app',
directives: [AppBarComponent, BUTTON_DIRECTIVES, CORE_DIRECTIVES, FORM_DIRECTIVES],
template: `
<app-bar></app-bar>
<button type="button" class="btn btn-primary"
[(ngModel)]="singleModel" btnCheckbox
btnCheckboxTrue="1" btnCheckboxFalse="0">
Single Toggle
</button>
singleModel: {{singleModel}}
`
})
export class AppComponent { }