I'm implementing an application with Vue Js and I've the following code:
<template>
<simple-page title="list-patient" folder="Patient" page="List Patient" :loading="loading">
<list-patients #patientsLoaded="onPatientsLoaded"/>
</simple-page>
</template>
Both simple-page and list-patients are custom components created by me. Inside ListPatients I've an HTTP request on Create callback, as follows:
created() {
axios.get("...").then(response => {
...
this.$emit('patientsLoaded');
})
},
Then, my objective is to handle the patientsLoaded event and uptade the loading prop on the top parent component, as follows:
data() {
return {
loading: true
}
},
methods: {
onPatientsLoaded(params) {
this.loading = false;
}
}
However, the created method is not being triggered inside the list-patients component. The only way I can make this work is by removing :loading.
Any one can help?
Edit 1
Code of simple page:
<template>
<section :id="id">
<!-- Breadcrumb-->
<breadcumb :page="page" :folder="folder"/>
<!-- Breadcrumb-->
<!-- Simple Card-->
<simple-card :title="page" :icon="icon" :loading="loading" v-slot:body>
<slot>
</slot>
</simple-card>
<!-- Simple Card-->
</section>
</template>
Code of simple card:
<b-card>
<!-- Page body-->
<slot name="body" v-if="!loading">
</slot>
<!--Is loading-->
<div class="loading-container text-center d-block">
<div v-if="loading" class="spinner sm spinner-primary"></div>
</div>
</b-card>
Your list-patients component goes in the slot with name "body". That slot has a v-if directive so basically it is not rendered and hooks are not reachable as well. Maybe changing v-if to v-show will somehow help you in that situation. Anyway, you have deeply nested slots and it is making things messy. I usually declare loading variable inside of the component, where fetching data will be rendered.
For example:
data () {
return {
loading: true;
};
},
mounted() {
axios.get('url')
.then(res => {
this.loading = false;
})
}
and in your template:
<div v-if="!loading">
<p>{{fetchedData}}</p>
</div>
<loading-spinner v-else></loading-spinner>
idk maybe that's not best practise solution
v-slot for named slots can be indicated in template tag only
I suppose you wished to place passed default slot as body slot to simple-card component? If so you should indicate v-slot not in simple-card itself but in a content you passed it it.
<simple-card :title="page" :icon="icon" :loading="loading">
<template v-slot:body>
<slot>
</slot>
</template>
</simple-card>
Related
I'm creating a dashboard with Laravel and VueJS, I created a button that allows to enlarge or reduce my sidebar in a Sidebar.vue component here is my components:
<template>
<aside :class="`${is_expanded ? 'is-expanded' : ''}`">
<div class="head-aside">
<div class="app-logo">
<i class="bx bxl-trip-advisor"></i>
</div>
<span class="app-name">CONTROLPANEL</span>
</div>
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="ToggleMenu()">
<span class="boxicons"><i class="bx bx-chevrons-right"></i></span>
</button>
</div>
</aside>
</template>
<script>
export default {
data() {
return {
is_expanded: ref(localStorage.getItem('is_expanded') === 'true'),
}
},
methods: {
ToggleMenu() {
this.is_expanded = !this.is_expanded
localStorage.setItem('is_expanded', this.is_expanded)
},
},
}
</script>
The problem that arises is that in another component I created a navbar with a fixed width, what I would like to do is that when my sidebar changes size I would like my navbar to also change, in the other component I just have a template with a nav and the import of my sidebar.
You say you are using laravel with vue. It's not clear how the vue components are integrated, but I'm going to assume that laravel injects individual vue components that you would like to communicate. (as opposed to a vue based SPA that communicates with laravel using API only)
The two components are not aware of each other. Even though they both have access to the same localStorage, they don't know when the values there are updated.
There are several ways you can deal with this, here's one way
create a reactive object outside of the components to manage the shared state
import { ref, computed } from 'vue'
const isExpandedRef = ref(localStorage.getItem('is_expanded') === 'true');
window.IS_EXPANDED = computed({
get(){
return isExpandedRef.value
},
set(value){
isExpandedRef.value = !!value;
localStorage.setItem('is_expanded', isExpandedRef.value);
}
})
the ref is required so that when isExpandedRef.value changes, the computed getter triggers notifications to it's listeners.
If you load this before any of the components, you will create a computed(reactive) variable available to any script on the page (frames and shadow dom aside)
then you can use in your components like this.
<template>
<aside :class="`${is_expanded ? 'is-expanded' : ''}`">
<div class="head-aside">
<div class="app-logo">
<i class="bx bxl-trip-advisor"></i>
</div>
<span class="app-name">CONTROLPANEL</span>
</div>
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="is_expanded = !is_expanded">
<span class="boxicons"><i class="bx bx-chevrons-right"></i></span>
</button>
</div>
</aside>
</template>
<script>
export default {
data() {
return {
is_expanded: window.IS_EXPANDED,
}
},
}
</script>
because is_expanded is a computed with with getters and setters, you can set the value straight from the template, or use this.is_expanded = !this.is_expanded in methods.
As stated already, this relies on using the global window object. This solution is proposed for its simplicity. There are some drawbacks to using the window object, and a more robust solution would rely on injecting such shared state instead of relying on window, but it comes with more overhead.
This code works fine
<template>
<aside :class="`${is_expanded ? 'is-expanded' : ''}`">
<div class="head-aside">
<div class="app-logo">
<i class="bx bxl-trip-advisor"></i>
</div>
<span class="app-name">CONTROLPANEL</span>
</div>
<div class="menu-toggle-wrap">
<button class="menu-toggle" #click="ToggleMenu()">
<span class="boxicons">
<i class="bx bx-chevrons-right"></i>click me
</span>
</button>
</div>
</aside>
</template>
<script>
export default {
data() {
return {
is_expanded: localStorage.getItem("is_expanded") === "true",
};
},
methods: {
ToggleMenu() {
this.is_expanded = !this.is_expanded;
localStorage.setItem("is_expanded", this.is_expanded);
},
},
};
</script>
Here is a GIF showing the result in action: https://share.cleanshot.com/TtKsUj
Make local storage reactive by using watcher and setting its deep property to true
https://vuejs.org/guide/essentials/watchers.html#deep-watchers
or if you are using vue2 then use event bus
I've created three simple buttons that will trigger three different bootstrap modal dialog. The modal dialogs are "Add Product", "Edit Product" and "Delete Product". Both the Add and Edit modal dialogs contain a form with two input elements, whereas the Delete modal dialog contains a simple text. I realise that my code becomes very messy and hard to maintain. Hence, I have the following question:
1) How do I reuse the modal dialog, instead of creating 3 separate dialogs?
2) How do I know which modal dialog has been triggered?
Update: I've developed a soultion where I will include conditional statements such as v-if, v-else-if and v-else to keep track of which button the user click. However, I still feel that there is a better solution to this. Can anyone help/advice me?
Below is my current code:
<template>
<div>
<b-button v-b-modal.product class="px-4" variant="primary" #click="addCalled()">Add</b-button>
<b-button v-b-modal.product class="px-4" variant="primary" #click="editCalled()">Edit</b-button>
<b-button v-b-modal.product class="px-4" variant="primary" #click="deleteCalled()">Delete</b-button>
<!-- Modal Dialog for Add Product -->
<b-modal id="product" title="Add Product">
<div v-if="addDialog">
<form #submit.stop.prevent="submitAdd">
<b-form-group id="nameValue" label-cols-sm="3" label="Name" label-for="input-horizontal">
<b-form-input id="nameValue"></b-form-input>
</b-form-group>
</form>
<b-form-group id="quantity" label-cols-sm="3" label="Quantity" label-for="input-horizontal">
<b-form-input id="quantity"></b-form-input>
</b-form-group>
</div>
<div v-else-if="editDialog">
<form #submit.stop.prevent="submitEdit">
<b-form-group id="nameValue" label-cols-sm="3" label="Name" label-for="input-horizontal">
<b-form-input id="nameValue" :value="productName"></b-form-input>
</b-form-group>
</form>
<b-form-group id="quantity" label-cols-sm="3" label="Quantity" label-for="input-horizontal">
<b-form-input id="quantity" :value="productQuantity">5</b-form-input>
</b-form-group>
</div>
<div v-else>
<p class="my-4">Are You Sure you want to delete product?</p>
</div>
</b-modal>
</div>
</template>
<script>
export default {
data() {
return {
productName: "T-Shirt",
productQuantity: 10,
addDialog: false,
editDialog: false,
deleteDialog: false
};
},
methods: {
addCalled() {
this.addDialog = true;
},
editCalled() {
this.editDialog = true;
this.addDialog = false;
this.deleteDialog = false;
},
deleteCalled() {
this.deleteDialog = true;
this.addDialog = false;
this.editDialog = false;
}
}
};
</script>
<style>
</style>
As already mentionned, I would have use slots and dynamic component rendering to accomplish what you're trying to do in a cleaner way.
See snippet below (I didn't make them modals as such but the idea is the same).
This way, you can have a generic modal component that deals with the shared logic or styles and as many modalContentsub-components as needed that are injected via the dedicated slot.
Vue.component('modal', {
template: `
<div>
<h1>Shared elements between modals go here</h1>
<slot name="content"/>
</div>
`
});
Vue.component('modalA', {
template: `
<div>
<h1>I am modal A</h1>
</div>
`
});
Vue.component('modalB', {
template: `
<div>
<h1>I am modal B</h1>
</div>
`
});
Vue.component('modalC', {
template: `
<div>
<h1>I am modal C</h1>
</div>
`
});
new Vue({
el: "#app",
data: {
modals: ['modalA', 'modalB', 'modalC'],
activeModal: null,
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button v-for="modal in modals" #click="activeModal = modal"> Open {{ modal }} </button>
<modal>
<template slot="content">
<component :is="activeModal"></component>
</template>
</modal>
</div>
Update
Now, You might think how will you close your modal and let the parent component know about it.
On click of button trigger closeModal for that
Create a method - closeModal and inside commonModal component and emit an event.
closeModal() {
this.$emit('close-modal')
}
Now this will emit a custom event which can be listen by the consuming component.
So in you parent component just use this custom event like following and close your modal
<main class="foo">
<commonModal v-show="isVisible" :data="data" #close- modal="isVisible = false"/>
<!-- Your further code -->
</main>
So as per your question
A - How do I reuse the modal dialog, instead of creating 3 separate dialogs
Make a separate modal component, let say - commonModal.vue.
Now in your commonModal.vue, accept single prop, let say data: {}.
Now in the html section of commonModal
<div class="modal">
<!-- Use your received data here which get received from parent -->
<your modal code />
</div>
Now import the commonModal to the consuming/parent component. Create data property in the parent component, let say - isVisible: false and a computed property for the data you want to show in modal let say modalContent.
Now use it like this
<main class="foo">
<commonModal v-show="isVisible" :data="data" />
<!-- Your further code -->
</main>
The above will help you re-use modal and you just need to send the data from parent component.
Now second question will also get solved here How do I know which modal dialog has been triggered?
Just verify isVisible property to check if modal is open or not. If isVisible = false then your modal is not visible and vice-versa
I'm attempting to create components using Vue, so that I can remove a lot of duplicated HTML in a site I'm working on.
I have a <ym-menucontent> component, which within it will eventually have several other components, conditionally rendered.
While doing this I've hit a wall and so have simplified everything to get to the root of the problem.
When rendering the ym-menucontent component the first sub-component is the only one which gets rendered and I can't work out why or how to get around it...
<template id="menucontent">
<div>
<ym-categories :menuitem="menuitem"/>
<ym-rootmaps :menuitem="menuitem"/>
<p>1: {{menuitem.rootMapsTab}}</p>
<p>2: {{menuitem.exploreTab}}</p>
</div>
</template>
<template id="rootmaps">
<div>Root Maps</div>
</template>
<template id="categories">
<div>Categories</div>
</template>
app.js
Vue.component('ym-menucontent', {
template: '#menucontent',
props: ['menuitem'],
data: function() {
return {
customMenu: window.customMenuJSON
}
}
});
Vue.component('ym-rootmaps', {
template: '#rootmaps',
props: ['menuitem'],
data: function() {
return {
customMenu: window.customMenuJSON,
rootMaps: window.rootAreas
}
}
});
Vue.component('ym-categories', {
template: '#categories',
props: ['menuitem'],
data: function() {
return {
customMenu: window.customMenuJSON,
rootMaps: window.rootAreas
}
}
});
usage...
<div
v-for="mi in customMenu.topLevelMenuItems"
:id="mi.name"
class="page-content tab swiper-slide">
<ym-menucontent :menuitem="mi"/>
</div>
Output
<div>Categories</div>
if I switch around ym-cateogries and ym-rootmaps then the output becomes...
<div>Root Maps</div>
if I remove both then I see...
<p>1: true</p>
<p>2:</p>
I'd expect to see a combination of all of them...
<div>Categories</div>
<div>Root Maps</div>
<p>1: true</p>
<p>2:</p>
This is probably because you're using self-closing components in DOM templates, which is recommended against in the style-guide ..
Unfortunately, HTML doesn’t allow custom elements to be self-closing -
only official “void” elements. That’s why the strategy is only
possible when Vue’s template compiler can reach the template before
the DOM, then serve the DOM spec-compliant HTML.
This should work for you ..
<template id="menucontent">
<div>
<ym-categories :menuitem="menuitem"></ym-categories>
<ym-rootmaps :menuitem="menuitem"></ym-rootmaps>
<p>1: {{menuitem.rootMapsTab}}</p>
<p>2: {{menuitem.exploreTab}}</p>
</div>
</template>
<div
v-for="mi in customMenu.topLevelMenuItems"
:id="mi.name"
class="page-content tab swiper-slide">
<ym-menucontent :menuitem="mi"></ym-menucontent>
</div>
I've noticed that transition hooks only get fired when the <transition> element is the root element in my component template. Is this by design? Am I missing something?
in my App.vue I have this template:
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
<example v-if = "checked"></example>
My component example.vue:
<template lang="html">
<section class="example">
<transition
v-on:enter = "enter"
v-on:leave = "leave">
<div class = "transition-example"></div>
</transition>
</section>
</template>
<script lang="js">
export default {
name: 'example',
props: [],
mounted() {
},
data() {
return {
}
},
methods: {
enter: function (el, done) {
console.log("enter")
done()
},
leave: function(el, done) {
console.log("leave")
done()
}
}
}
</script>
<style scoped >
</style>
In this current exmaple the enter and leave hooks are never executed when toggling the checkbox.
If I would update the template of example.vue to make sure the <transitions> element is the root element (as shown below) the enter and leave hooks are called.
<template lang="html">
<transition
v-on:enter = "enter"
v-on:leave = "leave">
<div class = "transition-example"></div>
</transition>
</template>
I'd like to have more flexibility in where I put my <transition> element or have multiple transition element in a component, which all have their own hooks.
I'm assuming I am overlooking something that prevents me from doing this.
I've noticed that transition hooks only get fired when the element is the root element in my component template. Is this by design? Am I missing something?
It's because of this line <example v-if="checked"></example>. v-if is applied to real root element of component so when transition is in root, v-if applied to div inside transition and it works fine, but in your first case v-if applied to section which is not under transition. So to make transition work you should provide v-if in element wrapped with transition tag, you can pass checked as prop to indicate visibility:
App.vue
...
<example :visible="checked"></example>
...
Example.vue
<template lang="html">
<section class="example">
<transition
v-on:enter = "enter"
v-on:leave = "leave">
<div v-if="visible" class="transition-example"></div>
</transition>
</section>
</template>
I have a Vue component simplified below.
Here is the template
<template>
<slot></slot>
</template>
The slot may contain HTML, which is why I decided to use a slot rather than a prop which I would simply bind to. I'd like to keep it that way.
I have a method that gets new HTML from the server. I'd like to use this new HTML to update the slot. I'm not sure if slots are reactive and how I can accomplish this.
I can view the default slot using this.$slots.default[0], but I don't know how to update it with a string of HTML content. Simply assigning the string to the element is obviously incorrect, to .innerHtml does not work because it isn't an available function, and to .text doesn't work. I assume that even though the text element exists on the slot object, the element properties take precedence.
Per suggestion in comments, I've tried this along with a computer property.
<span v-html="messageContent"><slot></slot></span>
But now the problem is that it overwrites the slot passed to me.
How can I reactively update a slot with new HTML in Vue.JS?
I think your issue comes from a misunderstanding of how <slot> inherently works in VueJS. Slots are used to interweave content from a consuming parent component into a child component. See it as a HTML equivalent of v-bind:prop. When you use v-bind:prop on a component, you are effectively passing data into a child component. This is the same as slots.
Without any concrete example or code from your end, this answer is at best just guess-work. I assume that your parent component is a VueJS app itself, and the child component is the one that holds the <slot> element.
<!-- Parent template -->
<div id="app">
<custom-component>
<!-- content here -->
</custom-component>
</div>
<!-- Custom component template -->
<template>
<slot></slot>
</template>
In this case, the app has a default ground state where it passes static HTML to the child component:
<!-- Parent template -->
<div id="app">
<custom-component>
<!-- Markup to be interweaved into custom component -->
<p>Lorem ipsum dolor sit amet.</p>
</custom-component>
</div>
<!-- Custom component template -->
<template>
<slot></slot>
</template>
Then, when an event is fired, you want to replace that ground-state markup with new incoming markup. This can be done by storing the incoming HTML in the data attribute, and simply using v-html to conditionally render it. Let's say we want to store the incoming markup in app's vm.$data.customHTML:
data: {
customHTML: null
}
Then your template will look like this:
<!-- Parent template -->
<div id="app">
<custom-component>
<div v-if="customHTML" v-html="customHTML"></div>
<div v-else>
<p>Lorem ipsum dolor sit amet.</p>
</div>
</custom-component>
</div>
<!-- Custom component template -->
<template>
<slot></slot>
</template>
Note that in contrast to the code you have tried, the differences are that:
It is the parent component (i.e. the consuming component) that is responsible for dictating what kind of markup to pass to the child
The child component is as dumb as it gets: it simply receives markup and renders it in the <slot> element
See proof-of-concept below:
var customComponent = Vue.component('custom-component', {
template: '#custom-component-template'
});
new Vue({
el: '#app',
data: {
customHTML: null
},
components: {
customComponent: customComponent
},
methods: {
updateSlot: function() {
this.customHTML = '<p>Foo bar baz</p>';
}
}
});
.custom-component {
background-color: yellow;
border: 1px solid #000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<h1>I am the app</h1>
<button type="button" #click="updateSlot">Click me to update slot content</button>
<custom-component>
<div v-if="customHTML" v-html="customHTML">
</div>
<div v-else>
<p>Lorem ipsum dolor sit amet.</p>
</div>
</custom-component>
</div>
<!-- custom-component template -->
<script type="text/template" id="custom-component-template">
<div class="custom-component">
<h2>I am a custom component</h2>
<!-- slot receives markup set in <custom-component> -->
<slot></slot>
</div>
</script>
Below is my solution though I don't like this opinion (load html into slot directly in current component level) because it breaks the rules for the slot. And I think you should do like this way (<component><template v-html="yourHtml"></template></component>), it will be better because Slot will focus on its job as Vue designed.
The key is this.$slots.default must be one VNode, so I used extend() and $mount() to get the _vnode.
Vue.config.productionTip = false
Vue.component('child', {
template: '<div><slot></slot><a style="color:green">Child</a></div>',
mounted: function(){
setTimeout(()=>{
let slotBuilder = Vue.extend({
// use your html instead
template: '<div><a style="color:red">slot in child</a></div>',
})
let slotInstance = new slotBuilder()
this.$slots.default = slotInstance.$mount()._vnode
this.$forceUpdate()
}, 2000)
}
})
new Vue({
el: '#app',
data() {
return {
test: ''
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<child><h1>Test</h1></child>
</div>