How to write a plugin that shows a modal popup using vue. Call should be made as a function() - javascript

I am trying to make a VueJS plugin that exports a global method, which when called, will popup a message with an input text field. Ideally, I want to be able to make the following call from any Vue component:
this.$disaplayMessageWithInput("Title","Body","Value");
And a popup should come on the screen.
I've tried building it but when the install() calls this.$ref., it isn't recognized:
DeleteConfirmation.vue
<template>
<b-modal size="lg" ref="deleteConfirmationModal" :title="this.title" header-bg-variant="danger" #ok="confirmDelete" #cancel="confirmCancel">
<p>
{{this.body}}
</p>
</b-modal>
</template>
<script>
export default {
data()
{
return {
title: null,
body: null,
valueCheck: null,
value: null
};
},
install(vue, options)
{
Vue.prototype.$deleteConfirmation = function(title, body, expectedValue)
{
this.title = title;
this.body = body;
this.valueCheck = expectedValue;
this.$refs.$deleteConfirmation.show()
}
},
}
</script>
app.js
import DeleteConfirmation from './components/global/DeleteConfirmation/DeleteConfirmation';
Vue.use(DeleteConfirmation);
The call I am trying to make is:
$vm0.$deleteConfirmation("title","body","val");
I get the below error at the run time:
app.js?id=c27b2799e01554aae7e1:33 Uncaught TypeError: Cannot read property 'show' of undefined
at Vue.$deleteConfirmation (app.js?id=c27b2799e01554aae7e1:33)
at <anonymous>:1:6
Vue.$deleteConfirmation # app.js?id=c27b2799e01554aae7e1:33
(anonymous) # VM1481:1
It looks like, this.$refs in DeleteConfirmation.vue is undefined.

Try to avoiding $ref with vue ( $ref is here for third party and some very special case )
$ref isn't reactive and is populate after the render ...
the best solution for me is using a event bus like this :
const EventBus = new Vue({
name: 'EventBus',
});
Vue.set(Vue.prototype, '$bus', EventBus);
And then use the event bus for calling function of your modal ...
(
this.$bus.on('event-name', callback) / this.$bus.off('event-name');
this.$bus.$emit('event-name', payload);
)
You can create a little wrapper around the bootstrap modal like mine
( exept a use the sweet-modal)
<template>
<div>
<sweet-modal
:ref="modalUid"
:title="title"
:width="width"
:class="klass"
class="modal-form"
#open="onModalOpen"
#close="onModalClose"
>
<slot />
</sweet-modal>
</div>
</template>
<script>
export default {
name: 'TModal',
props: {
eventId: {
type: String,
default: null,
},
title: {
type: String,
default: null,
},
width: {
type: String,
default: null,
},
klass: {
type: String,
default: '',
},
},
computed: {
modalUid() {
return `${this._uid}_modal`; // eslint-disable-line no-underscore-dangle
},
modalRef() {
return this.$refs[this.modalUid];
},
},
mounted() {
if (this.eventId !== null) {
this.$bus.$on([this.eventName('open'), this.eventName('close')], this.catchModalArguments);
this.$bus.$on(this.eventName('open'), this.modalRef ? this.modalRef.open : this._.noop);
this.$bus.$on(this.eventName('close'), this.modalRef ? this.modalRef.close : this._.noop);
}
},
beforeDestroy() {
if (this.eventId !== null) {
this.$off([this.eventName('open'), this.eventName('close')]);
}
},
methods: {
onModalOpen() {
this.$bus.$emit(this.eventName('opened'), ...this.modalRef.args);
},
onModalClose() {
if (this.modalRef.is_open) {
this.$bus.$emit(this.eventName('closed'), ...this.modalRef.args);
}
},
eventName(action) {
return `t-event.t-modal.${this.eventId}.${action}`;
},
catchModalArguments(...args) {
if (this.modalRef) {
this.modalRef.args = args || [];
}
},
},
};
</script>
<style lang="scss" scoped>
/deep/ .sweet-modal {
.sweet-title > h2 {
line-height: 64px !important;
margin: 0 !important;
}
}
</style>

AppModal.vue
<template>
<div class="modal-wrapper" v-if="visible">
<h2>{{ title }}</h2>
<p>{{ text }}</p>
<div class="modal-buttons">
<button class="modal-button" #click="hide">Close</button>
<button class="modal-button" #click="confirm">Confirm</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
visible: false,
title: '',
text: ''
}
},
methods: {
hide() {
this.visible = false;
},
}
}
</script>
Modal.js (plugin)
import AppModal from 'AppModal.vue'
const Modal = {
install(Vue, options) {
this.EventBus = new Vue()
Vue.component('app-modal', AppModal)
Vue.prototype.$modal = {
show(params) {
Modal.EventBus.$emit('show', params)
}
}
}
}
export default Modal
main.js
import Modal from 'plugin.js'
// ...
Vue.use(Modal)
App.vue
<template>
<div id="app">
// ...
<app-modal/>
</div>
</template>

This looks pretty complicated. Why don't you use a ready-to-use popup component like this one? https://www.npmjs.com/package/#soldeplata/popper-vue

Related

Active events in vue instance encapsulated inside shadowDOM

I'm searching to trigger event from a vue instance which is encapsulated inside a shadowDOM.
For the moment, my code create a new Vue instance inside a shadowDOM and print the template without the style (because nuxt load the style inside the base vue instance).
I can call methods of each instance
But, when i'm trying to add an event / listener on my component, it doesn't work.
DOM.vue
<template>
<section ref='shadow'></section>
</template>
<script>
import bloc from '#/components/bloc.vue'
import Vue from 'vue'
export default {
data() {
return {
vm: {},
shadowRoot: {},
isShadowReady: false
}
},
watch: {
isShadowReady: function() {
let self = this
this.vm = new Vue({
el: self.shadowRoot,
components: { bloc },
template: "<bloc #click='listenClick'/>",
methods: {
listenClick() { console.log("I clicked !") },
callChild() { console.log("I'm the child !") }
},
created() {
this.callChild()
self.callParent()
}
})
}
},
mounted() {
const shadowDOM = this.$refs.shadow.attachShadow({ mode: 'open' })
this.shadowRoot = document.createElement('main')
shadowDOM.appendChild(this.shadowRoot)
this.isShadowReady = true
},
methods: {
callParent() {
console.log("I'am the parent")
},
}
}
</script>
bloc.vue
<template>
<div>
<p v-for='word in words' style='text-align: center; background: green;'>
{{ word }}
</p>
</div>
</template>
<script>
export default {
data() {
return {
words: [1,2,3,4,5,6]
}
}
}
</script>
Any idea ?
Thank you
Hi so after i watched at your code i think it's better to use the $emit to pass event from the child to the parent and i think it's more optimized then a #click.native
template: "<bloc #someevent='listenClick'/>",
<template>
<div #click="listenClickChild">
<p v-for='(word, idx) in words' :key="idx" style='text-align: center; background: green;'>
{{ word }}
</p>
</div>
</template>
<script>
export default {
data() {
return {
words: [1,2,3,4,5,6]
}
},
methods: {
listenClickChild() {
this.$emit('someevent', 'someValue')
}
}
}
</script>
bloc #click.native='listenClick'/>

How to call a method in a Vue component from programmatically inserted component

I'm trying to call a method from a child component which is programatically inserted.
Here is my code.
MultipleFileUploader.vue
<template>
<div class="form-group" id="multiple-file-uploader">
<div>
<multiple-file-uploader-part
:name="uploadername" :index="1"
#remove="deleteUploader" #fileselected="fileSelected($event)">
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from './MultipleFileUploaderPart.vue';
let index_count = 1;
export default {
components: {
'multiple-file-uploader-part':MultipleFileUploaderPart,
},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
next_id:1,
}
},
methods: {
fileSelected: function (target) {
var UploaderPart = Vue.extend(MultipleFileUploaderPart);
new UploaderPart().$on('fileselected','fileSelected')
.$mount('#multiple-file-uploader');
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
<style scoped>
</style>
MultipleFileUploaderPart.vue
<template>
<div v-bind:id="name + '['+index+']'">
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="removeClicked"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
</div>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
removeClicked: function () {
document.getElementById(this.name+'[' + this.index + ']' ).remove();
this.$emit('remove', this.index);
},
fileSelectedMethod: function (target) {
this.size = target.files[0].size;
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
<style scoped>
I'm trying to achieve is that when a file input is filled with a file, a MultipleFileUploaderPart is created. And when the file input element in this component is filled, another MultipleFileUploaderPart is inserted.
I'd like to call MultipleFileUploader 's fileSelected method from newly inserted components so that I can create another component.
I also want to remove a MultipleFileUploaderPart component when the delete button is clicked.
How can I achieve this? or is there a better way?
EDIT:
This is what I originally had.
MultipleFileUploader.vue
<template>
<div class="form-group">
<div>
<multiple-file-uploader-part
v-for="uploader in uploaders"
:name="uploadername" :index="uploader.id"
#remove="deleteUploader" #fileselected="fileSelected($event)">
slot
</multiple-file-uploader-part>
</div>
</div>
</template>
<script>
import MultipleFileUploaderPart from "./MultipleFileUploaderPart";
let index_count = 1;
export default {
//name: "MultipleFileUploader",
components: {MultipleFileUploaderPart},
props: {
uploadername: {
type: String,
default: 'files',
}
},
data() {
return {
uploaders: [
{
id: index_count++,
},
]
}
},
methods: {
fileSelected: function (target) {
if(target.value){
this.uploaders.push({
id: index_count++,
})
}
},
deleteUploader: function (idToRemove) {
this.uploaders = this.uploaders.filter(
uploaders_id => {
return uploaders_id.id !== idToRemove;
}
)
}
},
}
</script>
MultipleFileUploaderPart.vue
<template>
<div class="input-group margin">
{{index}}
<input type="file" accept="application/pdf,image/jpeg,image/png"
v-bind:name="name + '['+index+']'"
v-on:change="fileSelectedMethod($event.target)">
<div class="input-group-btn">
<button #click="$emit('remove',index)"
class="btn btn-danger btn-sm"
v-if="index != 1"
type="button">
Delete{{index}}
</button>
</div>
<br>
<p v-if="size_error" style="color: red">File size must be less than 2MB</p>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
},
index: {
type: Number,
},
},
data() {
return {
size: '',
size_error: false,
}
},
methods: {
checkFileSize: function () {
},
fileSelectedMethod: function (target) {
console.log(target);
console.log(target.files);
this.size = target.files[0].size;
console.log(this.size);
if (this.size < 2000000) {
this.size_error = false;
this.$emit('fileselected', target);
} else {
target.value = null;
this.size_error = true;
console.log(target.files);
}
}
}
}
</script>
And this happens. please click
When I click 'Delete'Button, correct child coponent is deleted but the file in the input form stays there. that's why I'm seeking for another approach.
Declare uploaders as an array of objects that contain all needed props for creation of MultipleFileUploaderPart.
Use v-for on MultipleFileUploaderPart in the main MultipleFileUploader to reactively generate MultipleFileUploaderPart components
Use $emit from MultipleFileUploaderPart to MultipleFileUploader to emit creation and deletion events so that MultipleFileUploader can add or remove elements in the uploaders array.
Please don't delete or create elements from DOM directly, let the VueJs do this work.

How to pass initial form values to child component in Vue.js?

I'm using Vue.js. From my template I include the child component (componentB) which includes several input elements. I want to initialize those input elements from my parent template. I found a way to do this (see code below). However, I'm wondering if this is a correct way, as the articles I have read so far use different approaches (e.g. with $emit):
https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
https://zaengle.com/blog/using-v-model-on-nested-vue-components
https://alligator.io/vuejs/add-v-model-support/
Can you confirm that my code below matches the Vue.js design concepts or are there flaws?
<template>
<div>
<div class="md-layout">
<div class="md-layout-item md-size-100">
<ComponentB ref="componentB" v-model="componentB"></ComponentB>
</div>
</div>
</div>
</template>
<script>
import { ComponentB } from "#/components";
export default {
components: {
ComponentB
},
data() {
return {
componentB: {
textInputField: "my-initial-value"
}
};
},
methods: {
validate() {
return this.$refs.componentB.validate().then(res => {
this.$emit("on-validated", res);
return res;
});
}
}
};
</script>
<style></style>
Form componentB
<template>
<div>
<md-field
:class="[
{ 'md-valid': !errors.has('textInputField') && touched.textInputField },
{ 'md-form-group': true },
{ 'md-error': errors.has('textInputField') }
]"
>
<md-icon>label_important</md-icon>
<label>My text input</label>
<md-input
v-model="textInputField"
data-vv-name="textInputField"
type="text"
name="textInputField"
required
v-validate="modelValidations.textInputField"
>
</md-input>
<slide-y-down-transition>
<md-icon class="error" v-show="errors.has('textInputField')"
>close</md-icon
>
</slide-y-down-transition>
<slide-y-down-transition>
<md-icon
class="success"
v-show="!errors.has('textInputField') && touched.textInputField"
>done</md-icon
>
</slide-y-down-transition>
</md-field>
</div>
</template>
<script>
import { SlideYDownTransition } from "vue2-transitions";
export default {
name: "componentB",
props: ['value'],
components: {
SlideYDownTransition
},
computed: {
textInputField: {
get() {return this.value.textInputField},
set(textInputField) { this.$emit('input', { ...this.value, ['textInputField']: textInputField })}
}
},
data() {
return {
touched: {
textInputField: false
},
modelValidations: {
textInputField: {
required: true,
min: 5
}
}
};
},
methods: {
getError(fieldName) {
return this.errors.first(fieldName);
},
validate() {
return this.$validator.validateAll().then(res => {
return res;
});
}
},
watch: {
textInputField() {
this.touched.runnerName = true;
}
}
};
</script>
<style></style>
The simplest way to pass data to child component is to use props, which are then available in the child component and can pass the values back up to the parent.
https://v2.vuejs.org/v2/guide/components-props.html
// PARENT COMPONENT
<ComponentB :textInputField="textInputField" ...></ComponentB>
// CHILD COMPONENT
// TEMPLATE SECTION
<md-input
v-model="textInputField"
value="textInputField"
...
>
// SCRIPT SECTION
export default {
props: {
textInputField: String
}
}

Vue.js passing store data as prop causes mutation warnings

I'm passing store data (Vuex) as a property of component but it's giving me mutation errors even though I'm not changing the data.
Edit: Codepen illustrating error: https://codesandbox.io/s/v8onvz427l
Input
<template>
<div>
<input type="text" class="form-control" ref="input" />
<div style="padding-top: 5px">
<button #click="create" class="btn btn-primary btn-small">Create</button>
</div>
{{ example }}
</div>
</template>
<script>
import store from "#/store"
export default {
props: {
"example": {
}
},
data() {
return {
store
}
},
methods: {
create() {
store.commit("general_set_creation_name", {name: this.$refs.input.value})
}
}
}
</script>
Modal.vue
<template src="./Modal.html"></template>
<script>
import $ from 'jquery'
import store from '#/store'
export default {
props: {
"id": String,
"height": {
type: String,
default: "auto"
},
"width": {
type: String,
default: "40vw"
},
"position": {
type: String,
default: "absolute"
},
"component": {
default: null
},
"global": {
default: true
}
},
data () {
return {
store: store
}
},
computed: {
body () {
return store.state.General.modal.body
},
props () {
return store.state.General.modal.props
},
title () {
return store.state.General.modal.title
},
},
methods: {
close_modal (event) {
if (event.target === event.currentTarget) {
this.$refs.main.style.display = "none"
}
}
}
}
</script>
<style scoped lang="scss" src="./Modal.scss"></style>
Modal.html
<div
:id="id"
class="main"
ref="main"
#click="close_modal"
>
<div ref="content" class="content" :style="{minHeight: height, minWidth: width, position}">
<div ref="title" class="title" v-if="title">
{{ title }}
</div>
<hr v-if="title" />
<div ref="body" class="body">
<slot></slot>
<component v-if="global" :is="body" v-bind="props"></component>
</div>
</div>
</div>
Changing store data with this line in a third component:
store.commit("general_set_modal", {body: Input, title: "New "+page, props: {example: "example text 2"})
I'm quite sure you should not put a vue component on the state. If you are supposed to do that then I don't think the creators of vuex understand how an event store works.
In the documentation it also says you need to initialize your state with values and you don't do that.
Your sandbox works fine when removing the vue component from the state (state should contain data but vue components are objects with both data and behavior).
index.js in store:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
modal: {
body: {},
title: "",//det it to something
props: {}
},
creationName: null
},
mutations: {
general_set_creation_name(state, payload) {
state.creationName = payload.name;
},
general_set_modal(state, payload) {
state.modal.title = payload.title;
state.modal.props = payload.props;
console.log("we are fine here");
}
},
strict: process.env.NODE_ENV !== "production"
});
For whatever reason, changing the way I import the class removes the warning
const test = () => import('./Test')
Details:
https://forum.vuejs.org/t/getting-vuex-mutation-error-when-im-only-reading-the-data/27655/11

Vue JS Component V-Model $emit

I am using Vue JS 2 with Laravel 5.5. When I use a component and pass back the v-model within an $emit and then look to use the data on it's parent it returns 'undefined' in production. However if I use on my local dev environment it works fine?
The idea is that I can collect the data from the text editor and then store when saving the content.
I've tried "npm run development"
My component
<template>
<div>
<ckeditor
v-model="editorInfo.content"
:config="config"
#blur="onBlur($event)">
</ckeditor>
</div>
</template>
<script>
import Ckeditor from 'vue-ckeditor2';
export default {
components: { Ckeditor },
props: {
type: '',
content: ''
},
data () {
return {
content: '',
editorInfo: {
type: '',
content: this.content
},
config: {
height: 300,
}
}
},
methods: {
onBlur (editor) {
this.editorInfo.type = this.type;
this.$emit('update',this.editorInfo);
}
}
}
</script>
My Parent
import MyCkeditor from './../components/MyCkeditor';
new Vue({
el: '#vue',
components: { MyCkeditor },
data: {
category: {
title: '',
description: '',
extra_content: '',
}
}
methods: {
Update(value){
console.log(value);
if(value.type == 'description'){
this.category.description = value.content;
} else {
this.category.extra_content = value.content;
}
}
},
})
My HTML
<div class="form-group">
<label>Description</label>
<my-ckeditor :type="'description'" v-on:update="Update"></my-ckeditor>
</div>

Categories

Resources