Rendering VUE slot only if content matches specific value - javascript

I have two routes that render the same component but with different data coming from an API.
This component has a child component called <base-section> that has a v-if directive that checks if a specific slot has content or not (because if it has no content, I don't want the slot to show).
However, there might be more than one instance of this child component on the same parent component, and therefore, if one of the instances has content in the slot, but the other one doesn't, VUE will automatically assume that all slots have content.
Therefore, I would like to know if there is any way of checking the specific slot content of each instance and then compare it with the data coming from the API. Please find my code below:
Parent component (Content.vue)
<template>
<base-section
v-for="entry in this.entries"
:key="entry.title"
lang="lang-markup"
:title="entry.title"
>
<template v-slot:content>
<span v-html="entry.content"></span>
</template>
<template v-slot:examples>
<div v-html="entry.examples"></div>
</template>
<template v-slot:code>
{{ entry.code }}
</template>
</base-section>
</template>
Child component (BaseSection.vue)
<template>
<hr class="my-6" />
<h4 class="text-salmon">{{ title }}</h4>
<section>
<div class="section-sm txt-justify" v-if="this.$slots.content">
<slot name="content"></slot>
</div>
<span class="medal bg-light text-dark code-medal">Examples</span>
<div class="section-sm border-light-1 mb-3">
<slot name="examples"></slot>
</div>
<span class="medal text-dark code-medal">Code</span>
<pre :class="lang + ' border-light-1 bg-light'">
<code><slot name="code"></slot></code>
</pre>
</section>
</template>
The data coming from the API follows this structure:
getData() {
const url = this.apiUrl + this.$route.name + this.apiToken
fetch(url)
.then((collection) => collection.json())
.then((collection) => {
const entries = [];
this.entries = [];
for (const id in collection.entries) {
if (collection.entries[id].Version === this.byteVersion) {
entries.push({
title: collection.entries[id].Title,
content: collection.entries[id].Content,
examples: collection.entries[id].Examples,
code: collection.entries[id].Code,
});
}
}
this.entries = entries;
});
}
Thank you very much for your help!
Regards,
T.

Maybe you can pass the "entry.content" into your BaseSection component. and v-if the entryContent.
Parent component (Content.vue)
<template>
<base-section
v-for="entry in this.entries"
:key="entry.title"
lang="lang-markup"
:title="entry.title"
:entryContent="entry.content"
>
<template v-slot:content>
<span v-html="entry.content"></span>
</template>
<template v-slot:examples>
<div v-html="entry.examples"></div>
</template>
<template v-slot:code>
{{ entry.code }}
</template>
</base-section>
</template>
Child component (BaseSection.vue)
<div class="section-sm txt-justify" v-if="entryContent">
<slot name="content"></slot>
</div>
Or you can v-if your content template
<template v-slot:content v-if="entry.content">
<span v-html="entry.content"></span>
</template>

Related

Vue.js 3 - inserting component into slot

What I'm trying to achieve
I'm trying to pass Component into the slot.
The question/information
How do I pass the Component into the slot so that It will be rendered? This works fine as long as I pass strings / plain html.
Additional question
If this is not possible - then how can I pass component into other component with the structure like below?
Parent
Template code
<template>
<card-with-title card-title="Title">
<template #card-body>
<row-fontawesome-icon-with-text v-for="mailDto in lastProcessedEmails"/>
</template>
</card-with-title>
</template>
Script code - the important part
<script>
import SymfonyRoutes from '../../../../../core/symfony/SymfonyRoutes';
import GetLastProcessedEmailsResponseDto from '../../../../../core/dto/api/internal/GetLastProcessedEmailsResponseDto';
import MailDto from '../../../../../core/dto/modules/mailing/MailDto';
import CardWithTitleComponent from '../../../../base-layout/components/cards/card-with-title';
import RowFontawesomeIconWithTextComponent from '../../../../other/row-fontawesome-icon-with-text';
export default {
components: {
'card-with-title' : CardWithTitleComponent,
'row-fontawesome-icon-with-text' : RowFontawesomeIconWithTextComponent,
},
<...>
Child
<!-- Template -->
<template>
<div class="col-12 col-lg-4 mb-4">
<div class="card border-light shadow-sm">
<div class="card-header border-bottom border-light">
<h2 class="h5 mb-0">{{ cardTitle }}</h2>
</div>
<div class="card-body">
<slot name="card-body"></slot>
<slot></slot>
</div>
</div>
</div>
</template>
<!-- Script -->
<script>
export default {
props: [
"cardBody",
"cardStyle",
"cardTitle"
],
}
</script>
I did research about the question, I've seen in documentation how does the named slots work like, but non of the posts / blogs entries answer / solve my Problem.
Examples of checked resources:
https://www.smashingmagazine.com/2019/07/using-slots-vue-js/
How to insert named slots into parent components
https://medium.com/js-dojo/vue-named-slot-shorthand-8a920358e861
https://v3.vuejs.org/guide/component-slots.html
https://medium.com/#norton.seanm/vue-js-slots-8a274c80450e
I've found the solution... It's pretty much... terrifying.
Vue is not checking if the array is empty, on the v-for it tries to loop over and then throws error.
Personally, from other languages / frameworks - this should not happen.
But well, this is the solution:
<!-- Template -->
<template>
<card-with-title card-title="Title">
<template #card-body>
<div v-if="[lastProcessedEmails.length]">
<row-fontawesome-icon-with-text v-for="mailDto in lastProcessedEmails">
<template #icon>
<i class="font-weight-bold">
<i v-if="mailDto.status === mailStatusSent" :class="fontawesomeIconClassesSent"></i>
<i v-else-if="mailDto.status === mailStatusPending" :class="fontawesomeIconClassesPending"></i>
<i v-else :class="fontawesomeIconClassesError"></i>
</i>
</template>
<template #title>
{{ mailDto.subject }}
</template>
<template #title-context>
{{ mailDto.created }}
</template>
</row-fontawesome-icon-with-text>
</div>
</template>
</card-with-title>
</template>
The whole problem was that:
data(){
return {
lastProcessedEmails: [],
}
},
The lastProcessedEmails is updated via Axios Call.

Is there a vue.js equivalent of ngTemplateOutlet?

Does vue.js have an equivalent of Angular's *ngTemplateOutlet directive? Let's say I have some components defined like this:
<template>
<div id="independentComponent">
Hello, {{firstName}}!
</div>
</template>
<script>
export default {
name: "independentComponent",
props: ['firstName']
}
</script>
...
<template>
<div id="someChildComponent">
<slot></slot>
<span>Let's get started.</span>
</div>
</template>
<script>
export default {
name: "someChildComponent"
}
</script>
I want to be able to do something like this:
<template>
<div id="parentComponent">
<template #indepdentInstance>
<independentComponent :firstName="firstName" />
</template>
<someChildComponent>
<template #indepdentInstance></template>
</someChildComponent>
</div>
</template>
<script>
export default {
name: "parentComponent",
components: {
someChildComponent,
independentComponent
},
data() {
return {
firstName: "Bob"
}
}
}
</script>
In Angular, I could accomplish this with
<div id="parentComponent">
<someChildComponent>
<ng-container *ngTemplateOutlet="independentInstance"></ng-container>
</someChildComponent>
<ng-template #independentInstance>
<independentComponent [firstName]="firstName"></independentComponent>
</ng-template>
</div>
But it looks like Vue requires the element to be written to the DOM exactly where it is in the template. Is there any way to reference an element inline and use that to pass to another component as a slot?
You cannot reuse templates like ngTemplateOutlet, but can combine idea of $refs, v-pre and runtime template compiling with v-runtime-template to achieve this.
First, create reusable template (<ng-template #independentInstance>):
<div ref="independentInstance" v-show="false">
<template v-pre> <!-- v-pre disable compiling content of template -->
<div> <!-- We need this div, because only one root element allowed in templates -->
<h2>Reusable template</h2>
<input type="text" v-model="testContext.readWriteVar">
<input type="text" v-model="readOnlyVar">
<progress-bar></progress-bar>
</div>
</template>
</div>
Now, you can reuse independentInstance template:
<v-runtime-template
:template="$refs.independentInstance.innerHTML"
v-if="$refs.independentInstance">
</v-runtime-template>
But keep in mind that you cannot modify readOnlyVar from inside independentInstancetemplate - vue will warn you with:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "readOnlyVar"
But you can wrap it in object and it will work:
#Component({
components: {
VRuntimeTemplate
}
})
export default class ObjectList extends Vue {
reusableContext = {
readWriteVar: '...'
};
readOnlyVar = '...';
}
You could try Portal vue written by LinusBorg a core Vue team member.
PortalVue is a set of two components that allow you to render a
component's template (or a part of it) anywhere in the document - even
outside the part controlled by your Vue App!
Sample code:
<template>
<div id="parentComponent">
<portal to="independentInstance">
<!-- This slot content will be rendered wherever the <portal-target>
with name 'independentInstance' is located. -->
<independent-component :first-name="firstName" />
</portal>
<some-child-component>
<portal-target name="independentInstance">
<!--
This component can be located anywhere in your App.
The slot content of the above portal component will be rendered here.
-->
</portal-target>
</some-child-component>
</div>
</template>
There is also a vue-simple-portal written by the same author that is smaller but that mounts the component to end of body element.
My answer from #NekitoSP gave me an idea for a solution. I have implemented the sample below. It worked for me. Perhaps you want to use it as a custom component with props.
keywords: #named #template #vue
<template>
<div class="container">
<div ref="templateRef" v-if="false">write here your template content and add v-if for hide in current place</div>
....some other contents goes here
<p v-html="getTemplate('templateRef')"></p>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
Vue.extend({
methods:{
getTemplate(tempRef){
return this.$refs[tempRef].innerHTML
}
}
})
</script>
X-Templates
Use an x-template. Define a script tag inside the index.html file.
The x-template then can be referenced in multiple components within the template definition as #my-template.
Run the snippet for an example.
See the Vue.js doc more information about x-templates.
Vue.component('my-firstname', {
template: '#my-template',
data() {
return {
label: 'First name'
}
}
});
Vue.component('my-lastname', {
template: '#my-template',
data() {
return {
label: 'Last name'
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-firstname></my-firstname>
<my-lastname></my-lastname>
</div>
<script type="text/x-template" id="my-template">
<div>
<label>{{ label }}</label>
<input />
</div>
</script>
Not really sure i understand your problem here, but i'll try to give you something that i will opt to do if i want to add two components in one template.
HeaderSection.vue
<template>
<div id="header_id" :style="'background:'+my_color">
welcome to my blog
</div>
</template>
<script>
export default {
props: ['my_color']
}
</script>
BodySection.vue
<template>
<div id="body_id">
body section here
</div>
</template>
<script>
export default {
}
</script>
home.vue
<template>
<div id="parentComponent">
<header-section :color="my_color" />
<body-section />
</div>
</template>
<script>
import HeaderSection from "./components/HeaderSection.vue"
import BodySection from "./components/BodySection.vue"
export default {
name: "home",
components: {
HeaderSection,
BodySection
},
data() {
return {
my_color: "red"
}
}
}
</script>

Dynamically add a child component in Vue JS

I need some help in Vue JS and Laravel with adding a child vue component.
I have a parent component called "wrapper" and some child components called like "show-1", "show-2", "show-3" ... etc.
Parent component:
<template>
<div class="card border-light">
<div class="card-header">
<h5 class="title">{{ title }}</h5>
</div>
<div class="card-body">
<component
is="view"
></component >
</div>
<div class="card-footer"></div>
</div>
</template>
<script>
export default {
props : ['title'],
data() {
return {
view : ''
}
}
}
</script>
An exmaple child component like "show-1":
<template>
<div> show-1 </div>
</template>
This code below is in blade for rendering wrapper component with a dynamic child component name:
<wrapper
title="Example"
view="show-1"
></wrapper>
This code is not working but if i change the parent view data "show-1" instead of empty, it works. why ?
When I change the view prop, child vue component should be changed too. How could I do this ?
I want to pass the view attribute to parent component dynamically.
You can use :is attribute. You can read more about it here:
https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components
You can use the same mount point and dynamically switch between
multiple components using the reserved element and
dynamically bind to its is attribute....
<template>
<div class="card border-light">
<div class="card-header">
<h5 class="title">{{ title }}</h5>
</div>
<div class="card-body">
<!-- make sure to use : -->
<component v-if="view" :is="view"></component >
</div>
<div class="card-footer"></div>
</div>
</template>
<script>
export default {
props : ['title'],
data() {
return {
view : ''
}
}
}
</script>
#Eduardo has the right answer. To add to it, import your components into the parent and switch between them via a data property:
...
<component :is="current"></component >
...
data: {
current: 'show1'
},
components: {
show1: Show1Component,
show2: Show2Component,
show3: Show3Component
}
The key is to bind the component using the name of the dynamic component.
https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components

Polymer DOM-repeat content depending on received data

I have made a DOM-repeat that recieves multiple types of data.
Now I want the DOM-repeat to only show the items related to that type.
Is it possible to do this and how would I be able to achieve this?
When the item.Type is Zone I want it to display the item.Name and item.ZoneID
when the item.Type is Building I want it to display the item.BuildingID, item.Address1 and item.Address2
This is my code:
<template is="dom-repeat" items="[[data]]">
<paper-card id="departmentspaperContainer2">
<div class="card-content">
<h3>Type: {{item.Type}}</h3>
<hr/>
<h4>Name: {{item.Name}}</h4>
<h4>BuildingID: {{item.BuildingID}}</h4>
<h4>ZoneID: {{item.ZoneID}}</h4>
<h4>Address1: {{item.Address1}}</h4>
<h4>Address2: {{item.Address2}}</h4>
</div>
</paper-card>
</template>
Something similar to what 'HakanC' is proposing, but differently, you can use the dom-if and bind the response of your filter function :
Here is a working jsfiddle (to use in chrome)
<dom-module id="os-test">
<template>
<template is="dom-repeat" items="{{data}}">
<paper-card id="departmentspaperContainer2">
<div class="cardContent" type="{{item.Type}}">
<h3>Type: [[item.Type]]</h3>
<hr/>
<template is='dom-if' if='{{showZone(item.Type)}}'>
<h4>Name: [[item.Name]]</h4>
<h4>ZoneID: [[item.ZoneID]]</h4>
</template>
<template is='dom-if' if='{{showBuilding(item.Type)}}'>
<h4>BuildingID:[[item.BuildingID]]</h4>
<h4>Address1: [[item.Address1]]</h4>
<h4>Address2: [[item.Address2]]</h4>
</template>
</div>
</paper-card>
</template>
</template>
</dom-module>
<script>
class OsTestElement extends Polymer.Element {
static get is() {
return 'os-test';
}
static get properties() {
return {
data: {
type: Array,
value: () => {
return [
{
Type:"Zone",
Name:"valueName1",
BuildingID:"valueBuildingID1",
ZoneID:"valueZoneID1",
Address1:"valueAddress11",
Address2:"valueAddress21",
},
{
Type:"Zone",
Name:"valueName2",
BuildingID:"valueBuildingID2",
ZoneID:"valueZoneID2",
Address1:"valueAddress12",
Address2:"valueAddress22",
},
{
Type:"Building",
Name:"valueName3",
BuildingID:"valueBuildingID3",
ZoneID:"valueZoneID3",
Address1:"valueAddress13",
Address2:"valueAddress23",
},
{
Type:"Building",
Name:"valueName4",
BuildingID:"valueBuildingID4",
ZoneID:"valueZoneID4",
Address1:"valueAddress14",
Address2:"valueAddress24",
}
]
}
}
}
}
showZone(item) {
return (item==="Zone");
}
showBuilding(item) {
return (item==="Building");
}
}
window.customElements.define(OsTestElement.is, OsTestElement);
</script>
You may use dom-if into dom-repeat as fallow.
<template is="dom-repeat" items="[[data]]">
<paper-card id="departmentspaperContainer2">
<div class="card-content">
<h3>Type: {{item.Type}}</h3>
<hr/>
<h4>Name: {{item.Name}}</h4>
<template is='dom-if' if='{{_show(item.Type)}}'>
<h4>ZoneID: {{item.ZoneID}}</h4>
</template>
<template is='dom-if' if='{{!_show(item.Type)}}'>
<h4>BuildingID: {{item.BuildingID}}</h4>
<h4>Address1: {{item.Address1}}</h4>
<h4>Address2: {{item.Address2}}</h4>
</template>
</div>
</paper-card>
</template>
....
_show(it){
return (it === "Zone");
}
With above logic should solve the problem.

Unable to access vue data in basic modal

I am trying to loop and reference vue data values within a modal and am getting an error that the field needs to be reactive, despite the values being provided in the component initialization. Any idea why this is?
Fiddle:
https://jsfiddle.net/mwLbw11k/2618/
JS
// register modal component
Vue.component('modal', {
template: '#modal-template'
})
// start app
new Vue({
el: '#app',
data: {
myData: [{'first': 'Matt', 'last': 'Smith'}, {'first': 'Tom', 'last': 'Brady'}],
showModal: false
}
})
HTML
<script type="text/x-template" id="modal-template">
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
default header
</slot>
</div>
<div class="modal-body">
<slot name="body">
<ul>
<li v-for="user in myData"> {{ user.first }} {{ user.last }}</li>
</ul>
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button class="modal-default-button" #click="$emit('close')">
OK
</button>
</slot>
</div>
</div>
</div>
</div>
</transition>
</script>
<!-- app -->
<div id="app">
<button id="show-modal" #click="showModal = true">Show Modal</button>
<!-- use the modal component, pass in the prop -->
<modal v-if="showModal" #close="showModal = false">
</modal>
</div>
Vue Warning
[Vue warn]: Property or method "myData" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
Yous should add myData to modal component props
Vue.component('modal', {
template: '#modal-template',
props: ['myData']
})
and then pass it from parent like this:
<modal v-if="showModal" :my-data="myData" #close="showModal = false">
</modal>
corrected demo from Bsalex
<modal v-if="showModal" :my-data="myData" #close="showModal = false">

Categories