Vue.js slots - how to retrieve slot content in computed properties - javascript

I have a problem with vue.js slots. On one hand I need to display the slot code. On the other hand I need to use it in a textarea to send it to external source.
main.vue
<template>
<div class="main">
<my-code>
<template v-slot:css-code>
#custom-css {
width: 300px
height: 200px;
}
</template>
<template v-slot:html-code>
<ul id="custom-css">
<li> aaa </li>
<li> bbb </li>
<li> ccc </li>
</ul>
</template>
</my-code>
</div>
</template>
my-code.vue
<template>
<div class="my-code">
<!-- display the code -->
<component :is="'style'" :name="codeId"><slot name="css-code"></slot></component>
<slot name="html-code"></slot>
<!-- send the code -->
<form method="post" action="https://my-external-service.com/">
<textarea name="html">{{theHTML}}</textarea>
<textarea name="css">{{theCSS}}</textarea>
<input type="submit">
</form>
</div>
</template>
<script>
export default {
name: 'myCode',
props: {
codeId: String,
},
computed: {
theHTML() {
return this.$slots['html-code']; /* The problem is here, it returns vNodes. */
},
theCSS() {
return this.$slots['css-code'][0].text;
},
}
}
</script>
The issues is that vue doesn't turn the slot content. It's an array of <VNode> elements. Is there a way to use slots inside the textarea. Or a way to retrieve slot content in the theHTML() computed property.
NOTE: I use this component in vuePress.

You need to create a custom component or a custom function to render VNode to html directly. I think that will be the simplest solution.
vnode to html.vue
<script>
export default {
props: ["vnode"],
render(createElement) {
return createElement("template", [this.vnode]);
},
mounted() {
this.$emit(
"html",
[...this.$el.childNodes].map((n) => n.outerHTML).join("\n")
);
},
};
</script>
Then you can use it to your component
template>
<div class="my-code">
<!-- display the code -->
<component :is="'style'" :name="codeId"
><slot name="css-code"></slot
></component>
<slot name="html-code"></slot>
<!-- send the code -->
<Vnode :vnode="theHTML" #html="html = $event" />
<form method="post" action="https://my-external-service.com/">
<textarea name="html" v-model="html"></textarea>
<textarea name="css" v-model="theCSS"></textarea>
<input type="submit" />
</form>
</div>
</template>
<script>
import Vnode from "./vnode-to-html";
export default {
name: "myCode",
components: {
Vnode,
},
props: {
codeId: String,
},
data() {
return {
html: "", // add this property to get the plain HTML
};
},
computed: {
theHTML() {
return this.$slots[
"html-code"
]
},
theCSS() {
return this.$slots["css-code"][0].text;
},
},
};
</script>
this thread might help How to pass html template as props to Vue component

Related

Keep track of child component values in parent without using emit?

I have a custom component called HobbyForm which is a simple form with two controls, a checkbox and an input, this component is being called from a parent component called Content, along with other similar 'form' components.
<template>
<form>
<div class="row align-items-center">
<div class="col-1">
<Checkbox id="isHobbyActive" :binary="true" v-model="isActive"/>
</div>
<div class="col-5">
<InputText id="hobby" placeholder="Hobby" type="text" autocomplete="off" v-model="hobby"/>
</div>
</div>
</form>
</template>
<script>
export default {
name: 'HobbyForm',
data() {
return {
hobby: {
isActive: false,
hobby: null
}
}
},
}
</script>
My Content component is something like:
<template>
<language-form></language-form>
<hobby-form v-for="(hobbie, index) in hobbies" :key="index" v-bind="hobbies[index]"></hobby-form>
<Button label="Add Hobby" #click="addHobby"></Button>
</template>
<script>
export default {
name: "Content",
components: {
LanguageForm,
HobbyForm
},
data() {
return {
language: '',
hobbies: [
{
isActive: false,
hobby: null
}
]
};
},
methods: {
addHobby() {
this.hobbies.push({
isActive: false,
hobby: null
});
}
},
};
</script>
The idea is to be able to add more instances of the HobbyForm component to add another hobby record to my hobby data property; but I don't know how to keep track of these values from my parent without using an emit in my child components, since I don't want to manually trigger the emit, I just want to have the data updated in my parent component.
How should I access my child component's data from my parent and add it to my array?
In the current form passing parent data into a child component via v-bind="hobbies[index]" makes no sense as the child component (HobbyForm) has no props so it does not receive any data from the parent...
To make it work:
Remove data() from the child HobbyForm
Instead declare a prop of type Object
Bind form items to the properties of that Object
Pass the object into each HobbyForm
<template>
<form>
<div class="row align-items-center">
<div class="col-1">
<Checkbox id="isHobbyActive" :binary="true" v-model="hobby.isActive"/>
</div>
<div class="col-5">
<InputText id="hobby" placeholder="Hobby" type="text" autocomplete="off" v-model="hobby.hobby"/>
</div>
</div>
</form>
</template>
<script>
export default {
name: 'HobbyForm',
props: {
hobby: {
type: Object,
required: true
}
}
}
</script>
Even tho props are designed to be one way only so child should not mutate prop value, this is something else as you do not mutate prop value, you are changing (via a v-model) the properties of the object passed via a prop (see the note at the bottom of One-Way Data Flow paragraph)
Also change the parent to:
<hobby-form v-for="(hobby, index) in hobbies" :key="index" v-bind:hobby="hobby"></hobby-form>
Demo:
const app = Vue.createApp({
data() {
return {
hobbies: [{
isActive: false,
hobby: null
}]
};
},
methods: {
addHobby() {
this.hobbies.push({
isActive: false,
hobby: null
});
}
},
})
app.component('hobby-form', {
props: {
hobby: {
type: Object,
required: true
}
},
template: `
<form>
<div class="row align-items-center">
<div class="col-1">
<input type="checkbox" id="isHobbyActive" v-model="hobby.isActive"/>
</div>
<div class="col-5">
<input type="text" id="hobby" placeholder="Hobby" autocomplete="off" v-model="hobby.hobby"/>
</div>
</div>
</form>
`
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.1.5/dist/vue.global.js"></script>
<div id='app'>
<hobby-form v-for="(hobby, index) in hobbies" :key="index" v-bind:hobby="hobby"></hobby-form>
<button #click="addHobby">Add Hobby</button>
<hr/>
<pre> {{ hobbies }} </pre>
</div>

Popup does not open in App.vue when clicking on a search result

My goal is when clicking on any search item to open the Popup.
Why does not work — this.$emit('openPopup', bookId); in the method selectBook(bookId)
There is a component of Results.vue, which displays search results by using Google Books API:
<template>
<div class="results">
<ul class="results-items">
<li
class="book"
#click="selectBook(result.id)"
>
<img
:src="'http://books.google.com/books/content?id=' + result.id + '&printsec=frontcover&img=1&zoom=1&source=gbs_api'"
class="cover"
>
<div class="item-info">
<div class="bTitle">{{ result.volumeInfo.title }}</div>
<div
class="bAutors"
v-if="result.volumeInfo.authors"
>
{{ result.volumeInfo.authors[0] }}
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ['result'],
data() {
return {
bookId: '',
};
},
methods: {
selectBook(bookId) {
this.$router.push(`bookId${bookId}`);
this.$store.dispatch('selectBook', bookId);
this.$emit('hideElements', bookId);
this.$emit('openPopup', bookId);
},
},
};
</script>
Rusults screenshot
Is the main App.vue, which I want to display Popup:
<template>
<div id="app">
<div class="container">
<Header />
<popup
v-if="isOpenPopup"
#closePopup="closeInfoPopup"
#openPopup="showModal"
/>
<router-view/>
</div>
</div>
</template>
<script>
import Header from '#/components/Header.vue';
import Popup from '#/components/Popup.vue';
import 'bootstrap/dist/css/bootstrap.css';
export default {
components: {
Header,
Popup,
},
data() {
return {
isOpenPopup: false,
};
},
methods: {
showModal() {
console.log('click');
this.isOpenPopup = true;
},
closeInfoPopup() {
this.isOpenPopup = false;
},
},
};
</script>
The component Popup.vue
<template>
<div class="popup">
<div class="popup_header">
<span>Popup name</span>
</div>
<div class="popup_content">
<h1>Hi</h1>
<slot></slot>
<button #click="closePopup">Close</button>
</div>
</div>
</template>
<script>
export default {
name: 'popup',
props: {},
data() {
return {};
},
methods: {
closePopup() {
this.$emit('closePopup');
},
},
};
</script>
You are listening on popup component but triggering events on Result
Don't remember that events are bound to component.
You have several options how to handle it
Add event listeners (like #openPopup) only on Result component and use portal-vue library to render popup on top level
use global events, this.$root.emit and listen also on this.$root. In this case you add event listener in mount() hook and don't forget unregister it in beforeDestroy() hook
you can use Vuex and popup visibility and related data in global application store provided by Vuex

Using V-IF hides the template in the DOM

Inside of the DetailsForm.Vue I am using v-if and then in the Dom is hidding all the template. I dont understand why.
<template>
<div class="DetailsForm">
this is a test
<input type="checkbox" v-model="checked">I have filled all this page<br>
<div class="test" v-if="!$.checked.required">
This field is required
</div>
<button :disabled="!checked">Button</button>
</div>
</template>
<script>
import { required } from 'vuelidate/lib/validators'
export default {
name: 'DetailsForm',
data: function () {
return {
checked: false
}
},
validation: {
checked: {
required
}
}
}

Vue Scoped Slots two way data binding between component and slot [duplicate]

This question already has an answer here:
update data in slot vuejs
(1 answer)
Closed 2 years ago.
I have a scoped slot. I need the content that am passing to the slot to be able to affect the parent template.
This is what i have so far:
Parent.vue
<template>
<div>
<slot :text="text" :msg="msg"/>
<p>{{text}}</p>
<p>{{msg}}</p>
</div>
</template>
<script>
export default {
name: "Parent",
data() {
return {
text: "",
msg: ""
};
}
};
</script>
App.vue
<template>
<parent>
<template #default="{ text, msg }">
<input type="text" v-model="text"/>
<input type="text" v-model="msg"/>
</template>
</parent>
</template>
<script>
import Parent from "./components/Parent";
export default {
name: "App",
components: {
Toolbar
},
}
This does'nt work. How can i do something of the sort?
This is not possible. You can't change props (in this case slot props) given to your component(App.vue), but you can do something like this, with a "handler" method.
Parent.vue
<template>
<div>
<slot :text="text" :msg="msg" :setValue="setValue" />
<p>{{ text }}</p>
<p>{{ msg }}</p>
</div>
</template>
<script>
export default {
name: 'Parent',
data() {
return {
text: '',
msg: ''
};
},
methods: {
// set the current value with a function
setValue(e) {
this[e.target.name] = e.target.value;
}
}
};
</script>
App.vue
<template>
<parent>
<template #default="{ text, msg, setValue }">
Text: {{ text }}<br />
Msg: {{ msg }}<br /><br />
<!-- I have named the input fields after the variables in your data object and have the "setValue" method triggered by #input. -->
<input type="text" name="text" #input="setValue" />
<input type="text" name="msg" #input="setValue" />
</template>
</parent>
</template>
<script>
import Parent from './components/Parent';
export default {
name: 'App',
components: {
Parent
}
};
</script>

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>

Categories