Trying to hide model when clicked outside has different effects - javascript

I have a vue modal component which I make visible/invisible depending on a boolean variable, I also want to hide the modal when the user clicks outside the modal, I do this by attaching a click listener and checking each click to see if its outside or inside however there must be something wrong with my approach becuase the modal automatically closes right after I click the button to make it visible.
This is my component:
<template>
<transition name="cart-tab">
<div class="ADMINsearch_maincontainer" v-show="filtersVisible" id="admin-search">
</div>
</transition>
</template>
<!--SCRIPTS-->
<script>
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex';
export default {
name: 'LAYOUTsearch',
computed:
{
...mapState('AdminPanel',['filtersVisible', 'panelSchema', 'theme', 'searchSchema']),
},
data(){
return {
specifiedElement:null
}
},
mounted()
{
console.log(this.$options.name+' component successfully mounted');
this.addListener();
},
methods:
{
addListener()
{
this.specifiedElement = document.getElementById('admin-search')
const self = this;
document.addEventListener('click', function(event)
{
var isClickInside = self.specifiedElement.contains(event.target);
if (!isClickInside)
{
//outside
self.setFiltersVisible(false)
}
else
{
//inside
}
});
},
}
};
</script>
<!--STYLES-->
<style scoped>
.container_style{width:100% !important;}
.ADMINsearch_maincontainer{width:33%; height:100vh; z-index:9999999999999999; background-color:white; box-shadow:-3px -3px 6px 6px rgba(0,0,0,0.3); position:fixed; top:0px; right:0px; display:flex; flex-direction:column;}
}
</style>

The issue could be that you are getting the value of the variable filtersVisible trough mapState, then you are trying to update its value with a method in the component instance.
Let's suppose a store with this configuration:
const state = {
filtersVisible: false
}
const mutations = {
setFiltersVisibleValue(state, visible) {
state.filtersVisible = visible
}
}
export default {
state,
mutations,
}
In the component you can set and get the state of the filtersVisible variable this way:
<template>
<transition name="cart-tab">
<div
class="ADMINsearch_maincontainer"
v-show="filtersVisible"
id="admin-search"
></div>
</transition>
</template>
<!--SCRIPTS-->
<script>
import { mapState, mapMutations } from "vuex";
export default {
name: "LAYOUTsearch",
computed: {
...mapState(["filtersVisible"])
},
data() {
return {
specifiedElement: null
};
},
mounted() {
console.log(this.$options.name + " component successfully mounted");
this.addListener();
},
methods: {
...mapMutations(["setFiltersVisibleValue"]),
addListener() {
this.specifiedElement = document.getElementById("admin-search");
document.addEventListener("click", function(event) {
var isClickInside = self.specifiedElement.contains(event.target);
if (!isClickInside) {
//outside - call the mutation to update properly the value of the filtersVisible variable in the store
this.setFiltersVisibleValue(false);
} else {
//inside
}
});
}
}
};
</script>
<!--STYLES-->
<style scoped>
.container_style{width:100% !important;}
.ADMINsearch_maincontainer{width:33%; height:100vh; z-index:9999999999999999; background-color:white; box-shadow:-3px -3px 6px 6px rgba(0,0,0,0.3); position:fixed; top:0px; right:0px; display:flex; flex-direction:column;}
}
</style>
The example is using only the variable filtersVisible for the explanation purpose. I'm assuming a Vuex store that is not using modules.

Related

Vue: Teleport inheritAttrs false not working

I'm trying to create a BaseOverlay component that basically teleports its content to a certain area of my application. It works just fine except there's an issue when using it with v-show... I think because my component's root is a Teleport that v-show won't work because Teleport is a template.
I figured I could then use inheritAttrs: false and v-bind="$attrs" on the inner content... this throws a warning from Vue saying Runtime directive used on component with non-element root node. The directives will not function as intended. It results in v-show not working on MyComponent, but v-if does work.
Any clues as to what I'm doing wrong?
Example
App.vue
<script setup>
import MyComponent from "./MyComponent.vue";
import {ref} from "vue";
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div id="overlays" />
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Doesn't work" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
BaseOverlay.vue
<template>
<Teleport to="#overlays">
<div
class="overlay-container"
v-bind="$attrs"
>
<slot />
</div>
</Teleport>
</template>
<script>
export default {
name: "BaseOverlay",
inheritAttrs: false,
};
</script>
MyComponent.vue
<template>
<BaseOverlay>
{{text}}
</BaseOverlay>
</template>
<script>
import BaseOverlay from "./BaseOverlay.vue";
export default {
name: "MyComponent",
components: {
BaseOverlay
},
props: {
text: {
type: String,
default: ""
}
}
}
</script>
I would consider moving the modal/overlay dependency out of the component and into the app composition to make it more reusable.
Note the isMounted check check - this is to add a delay if the outlet containers have not yet been defined. You may need to add additional handling if your outlets are not pressent on mount e.g. <div id="outlet" v-if="false">
const {
createApp,
defineComponent,
ref,
onMounted
} = Vue
//
// Modal
//
const MyModal = defineComponent({
name: 'MyModal',
props: {
to: {
type: String,
required: true
},
show: Boolean
},
setup(){
const isMounted = ref(false);
onMounted(()=> isMounted.value = true )
return { isMounted }
},
template: `
<teleport :to="to" v-if="isMounted">
<div class="overlay-container" v-show="show">
<slot />
</div>
</teleport>
`
})
//
// Component
//
const MyComp = defineComponent({
name: 'MyComp',
template: `<div>Component</div>`
})
//
// App
//
const MyApp = defineComponent({
name: 'MyApp',
components: {
MyModal,
MyComp
},
setup() {
const modalShow = ref(false);
const modalOutlet = ref('#outletOne');
const toggleModalShow = () => {
modalShow.value = !modalShow.value
}
const toggleModalOutlet = () => {
modalOutlet.value = modalOutlet.value == '#outletOne'
? '#outletTwo'
: '#outletOne'
}
return {
toggleModalShow,
modalShow,
toggleModalOutlet,
modalOutlet,
}
},
template: `
<div>
<button #click="toggleModalShow">{{ modalShow ? 'Hide' : 'Show' }} Modal</button>
<button #click="toggleModalOutlet">Toggle Modal Outlet {{ modalOutlet }} </button>
</div>
<MyModal :to="modalOutlet" :show="modalShow">
<MyComp />
</MyModal>
<div id="outletOne">
<h2>Outlet One</h2>
<!-- outlet one -->
</div>
<div id="outletTwo">
<h2>Outlet Two</h2>
<!-- outlet two -->
</div>
`
})
//
// Assemble
//
const app = createApp(MyApp)
app.mount('body')
/* just some styling */
#outletOne { color: tomato; }
#outletTwo { color: olive; }
h2 { margin: 0; }
[id*=outlet]{ display: inline-flex; flex-direction: column; padding: 1rem; }
button { margin: 1rem; }
<script src="https://unpkg.com/vue#3.1.4/dist/vue.global.js"></script>
Wanted to follow-up on this. I started running into a lot of other issues with Teleport, like the inability to use it as a root element in a component (like a Dialog component because I want to teleport all dialogs to a certain area of the application), and some other strange issues with KeepAlive.
I ended up rolling my own WebComponent and using that instead. I have an OverlayManager WebComponent that is used within the BaseOverlay component, and every time a BaseOverlay is mounted, it adds itself to the OverlayManager.
Example
OverlayManager.js
export class OverlayManager extends HTMLElement {
constructor() {
super();
this.classList.add("absolute", "top-100", "left-0")
document.body.appendChild(this);
}
add(element) {
this.appendChild(element);
}
remove(element) {
this.removeChild(element);
}
}
customElements.define("overlay-manager", OverlayManager);
BaseOverlay.vue
<template>
<div class="overlay-container" ref="rootEl">
<slot />
</div>
</template>
<script>
import {ref, onMounted, onBeforeUnmount, inject} from "vue";
export default {
name: "BaseOverlay",
setup() {
const rootEl = ref(null);
const OverlayManager = inject("OverlayManager");
onMounted(() => {
OverlayManager.add(rootEl.value);
});
onBeforeUnmount(() => {
OverlayManager.remove(rootEl.value);
});
return {
rootEl
}
}
};
</script>
App.vue
<script setup>
import {OverlayManager} from "./OverlayManager.js";
import MyComponent from "./MyComponent.vue";
import {ref, provide} from "vue";
const manager = new OverlayManager();
provide("OverlayManager", manager);
const showOverlay = ref(false);
function onClickButton() {
showOverlay.value = !showOverlay.value;
}
</script>
<template>
<button #click="onClickButton">
Toggle Showing
</button>
<div>
Hello World
</div>
<MyComponent v-show="showOverlay" text="Now Works" />
<MyComponent v-if="showOverlay" text="Works" />
</template>
<style>
.absolute {
position: absolute;
}
.top-100 {
top: 100px;
}
.left-0 {
left: 0;
}
</style>
This behaves exactly how I need it, and I don't have to deal with the quirks that Teleport introduces, and it allows me to have a singleton that is in charge of all of my overlays. The other benefit is that I have access to the parent of where BaseOverlay is initially added in the HTML (not where it's moved). Honestly not sure if this is a good practice, but I'm chuffed at how cute this is and how well Vue integrates with it.

How to add style in Vue js 3 on a cdn based app

I'm learning Vue js. I created an app with Vue included from a CDN and I want to know how to add a <style> to it.
I can write template as template: `<div>Vue js 3</div>` but can a "style": " ... " string be written added to the component JS in the same way?
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
},
},
template: `<button #click="increment">count is: {{ count }}</button>`,
// can style be written like the template above? For example, I tried:
style: {
button {
background: red;
}
},
mounted() {
console.log(`The initial count is ${this.count}.`)
},
}
I think this should work for you.
<style>
#import './yourStyles.css';
</style>
If styles is particular to this component button, then you can use scoped style.
<style scoped>
button {
background : red;
}
</style>
If you want to apply common style for all the buttons in an application then you can create a common style file and then import wherever you want to access.
<style>
#import './button.css';
</style>
Demo :
new Vue({
el: '#app',
data: {
count: 0
},
methods: {
increment() {
this.count++
}
}
})
button {
background : red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="increment">count is: {{ count }}</button>
</div>

Call event bus with this.$root.$emit

I have a simple Event bus that changes styles on a page and it works. The event bus is called with the name of the event bus and $emit and $on:
EventBus.$on
and
EventBus.$emit('call-modal', { type: 'success' });
How can I make so that instead of calling it with $on and $emit I can call it with this.$root.$emit so that I can use it in every other component? I tried but currently it doesn't work, why?
Here is my App.vue:
<template >
<div id="app">
<bankAccount>
</bankAccount>
<div :class="['modal', `modal--type--${modalType}`]" v-show="showModal">
<slot name="title">e</slot>
<slot name="description"></slot>
</div>
</div>
</template>
<script>
import bankAccount from './components/bankAccount.vue'
import Vue from 'vue'
export const EventBus = new Vue()
export default {
name: 'app',
components: {
bankAccount,
},
data() {
return {
showModal: false,
modalType: 'default',
}
},
created() {
EventBus.$on('call-modal', obj => {
this.showModal = true
this.modalType = obj.type
})
},
}
</script>
<style>
.modal {
height: 100px;
width: 300px;
border: solid gray 2px;
}
.modal--type--success {
border-color: green;
}
.modal--type--danger {
border-color: red;
width: 100%;
}
.modal--type--warning {
border-color: yellow;
width: 100%;
}
</style>
And my component:
<template>
<div>
<button class="pleeease-click-me" #click="callModal()">Click me</button>
</div>
</template>
<script>
import { EventBus } from '../App.vue';
export default {
name: 'bankAccount',
data() {
return {
showModal: false
}
},
methods: {
callModal() {
this.showModal = !this.showModal
EventBus.$emit('call-modal', { type: 'success' });
}
}
}
</script>
<style scoped>
.modal {
height: 100px;
width: 300px;
}
</style>
You can create a separate eventbus.js file and include it to all of your components. This way, they'll be using the same eventbus instance. TL;DR: it's been explained here:
https://alligator.io/vuejs/global-event-bus/
Long one:
Create an eventbus.js file with this content:
import Vue from 'vue';
export const EventBus = new Vue();
Then include it:
import { EventBus } from './event-bus.js';
Then use it:
EventBus.$on("event", function(data){
// do stuff with data
});
with
EventBus.$emit("event", data);
and don't forget to remove the event on destruction:
beforeDestroy {
EventBus.$off("event");
}
Simply add that to the instance prototype :
// main.js
//import vue from 'vue'
Vue.prototype.$eventHub = new Vue();
// new Vue({
// ...
// })
now you can use it on any component with :
this.$eventHub.$emit('call-modal');
To achieve desired result you have to slightly change your code.
In your App.vue change these lines of code:
created() {
this.$root.$on('call-modal', obj => {
this.showModal = true
this.modalType = obj.type
})
},
And then in your component:
callModal() {
this.showModal = !this.showModal
this.$root.$emit('call-modal', { type: 'success' })
}
Notice that approach suggested by #Dadboz, is preferable, than you desirable.

Vue JS alert box

I'm new to VueJS.
I had written an alert box using jquery. Now I'm trying to provide that in VueJS.
Here is what I have done:
1- Created a component named NovinAlert.vue including:
<template>
<div id="novin-alert-container">
<div v-for="message in messages" class="alert novin-alert" :class="message.class">
×
{{ message.body }}
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
messages: [],
}
},
methods: {
novinAlert: function (message) {
let app = this;
message.type = typeof message.type !== 'undefined' ? message.type : 'info';
message.class = 'alert' + message.type;
let index = app.messages.push(message) - 1;
if (typeof message.duration !== 'undefined') {
window.setTimeout(function () {
app.messages.splice(index, 1);
}, message.duration * 1000);
}
}
}
}
</script>
2- My example component is Dashboard.vue
<template>
...
</template>
<script>
export default {
mounted() {
novinAlert('test');
}
}
</script>
3- My main layout is:
<body>
<div id="app">
<novin-alert></novin-alert>
<router-view></router-view>
</div>
</body>
4- And this is my app.js:
require('./bootstrap');
window.Vue = require('vue');
import VueRouter from 'vue-router';
window.Vue.use(VueRouter);
import Dashboard from './components/Dashboard.vue';
const routes = [
{path: '/', component: Dashboard, name: 'dashboard'},
];
import NovinAlert from './components/utils/NovinAlert.vue';
Vue.component('novin-alert', NovinAlert);
const router = new VueRouter({ routes: routes });
const app = new Vue({
router,
el: '#app'
});
When I run dashboard page, it doesn't know novinAlert function.
What I have done wrong? And what is best practice for what I'm trying to do?
Any help is appreciated.
i've designed a SIMPLE popup right now with Vuejs 2.* and it works well. In this example, popup is included as a Local component. This would be one of the best practices to follow. Let me know if this helps.
App.vue
<template>
<div>
<div>Main page</div>
<button #click="openpopup">Open popup</button>
<popup :popupData="popupData" ></popup>
</div>
</template>
<script>
import Popup from'./Popup.vue';
export default {
components:{
"popup" : Popup
},
data() {
return {
popupData : {
"header" : "My popup",
"body" : "hello world",
"footer" : "divine inc.",
"display" : "none"
}
}
},
methods : {
openpopup(){
this.popupData.display = "block";
}
},
mounted(){
this.popupData.display = "block";
}
}
</script>
Popup.vue
<template>
<div id="popup" :style="{display : popupData.display}">
<div class="inner">
<div class="header">
<div>{{popupData.header}}</div>
<div #click="closeFunction">Close</div>
</div>
<div class="body">{{popupData.body}}</div>
<div class="footer">{{popupData.footer}}</div>
</div>
</div>
</template>
<script>
export default {
props : ["popupData"],
methods : {
closeFunction(){
this.popupData.display = "none";
}
}
}
</script>
<style>
html,body{
padding: 0;
margin:0;
}
#popup{
position: absolute;
width: 100%;
height :100%;
top: 0;
}
#popup .inner{
background-color: green;
position: inherit;
top: 10%;
left: 39%;
width: 300px;
}
#popup .inner div{
text-align: left;
}
#popup .inner .header{
display: flex;
justify-content: space-between;
}
</style>
You can't access one component's method from another. you need to move the novinAlert function to the Dashboard component and pass messages as a property to NovinAlert component:
NovinAlert.vue
<template>
<div id="novin-alert-container">
<div v-for="message in messages" class="alert novin-alert" :class="message.class">
×
{{ message.body }}
</div>
</div>
</template>
<script>
export default {
props: ['messages']
}
</script>
Dashboard.vue
<template>
<!-- pass the messages prop to the component -->
<novin-alert :messages="messages"></novin-alert>
</template>
<script>
export default {
mounted() {
this.novinAlert('test');
},
data() {
return {
messages: []
}
},
methods: {
novinAlert: function (message) {
let app = this;
message.type = typeof message.type !== 'undefined' ? message.type : 'info';
message.class = 'alert' + message.type;
let index = app.messages.push(message) - 1;
if (typeof message.duration !== 'undefined') {
window.setTimeout(function () {
app.messages.splice(index, 1);
}, message.duration * 1000);
}
}
}
}
</script>
if you want the novin alert in the app level, you'll need to pass the function to the app component
This is what I finally did:
I created an instance of Vue in my app.js:
window.eventBus = new Vue();
I use this event but for multiple purposes. Whenever I need I trigger(emit) an event and the listener does required action.
For alert box, I emit the event like this:
window.eventBus.$emit('alert', {
text: app.trans('general.error_in_reading_data'),
type: 'danger',
duration: 5,
});
Then my alert component reacts like this:
mounted() {
window.eventBus.$on('alert', (message) => {
this.novinAlert(message);
});
},

Vue.js: Can't compile SASS styles for a dynamically-created HTML element

I have a simple Vue.js component in which I render a piece of HTML:
...
<div class="sml-button" v-on:click="toggleButton()" v-html="button"></div>
...
On click, the toggleButton() method updates the button data:
toggleButton: function() {
if (this.shouldBeShown) {
this.button = 'show less';
} else {
this.button = '<span>...</span>show more';
}
}
Notice the <span></span> element in the else.
What is happening, is I am unable to style this span element within my component - presumably because it's dynamically created:
<style lang="sass" scoped>
.sml-button {
color: #4390ca;
font-size: 14px;
font-family: latoregular, Helvetica, Arial, sans-serif;
span {
color: #3f3f3f; /* THIS WON'T BE COMPILED */
}
}
</style>
The parent class (.sml-button) has its styles. The child span doesn't.
How do I apply styles on a dynamically added HTML element inside of a Vue.js component?
Its working in root component and child component both
<template>
<template v-if="childDataLoaded">
<child-cmp :value="childdata"></child-cmp>
</template>
</template>
<script>
export default{
data(){
childDataLoaded: false,
childdata : [];
},
methods:{
fetchINitData(){
//fetch from server then
this.childdata = res.data.items;
this.childDataLoaded = true;
console.log(res.data.items) //has some values
}
}
components:{
childcmp
},
mounted(){
this.fetchINitData();
}
}
</script>
Here is the Nice and cleaner way to update child component.
var child = Vue.extend({
template: "<div>Child Component : <span class='light-blue'>My dynamicHTML</span></div>"
});
var app = new Vue({
el: "#vue-instance",
data: {
dynamicHTML:"Root Component : <span class='light-blue'>My dynamicHTML</span>"
},
methods : {
changeHTML: function(){
this.dynamicHTML = "Root Component Changed : <span class='light-green'>My dynamicHTML</span>"
}
},
components: {
child
},
})
.light-blue{
color : #f00;
}
.light-green{
color : #0f0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.1/vue.js"></script>
<div id="vue-instance">
<div v-html="dynamicHTML"></div>
<child></child>
<button v-on:click="changeHTML">Change HTML</button>
</div>

Categories