Is it possible to add content to a global Vue component from a single file comp? - javascript

I have made a global component that will render the content we want.
This component is very simple
<template>
<section
id="help"
class="collapse"
>
<div class="container-fluid">
<slot />
</div>
</section>
</template>
<script>
export default {
name: 'VHelp',
};
</script>
I use it inside my base template with
<v-help />
I'm trying to add content to this component slot from another single file component using.
<v-help>
<p>esgssthsrthsrt</p>
</v-help>
But this logically create another instance of my comp, with the p tag inside. Not the correct thing I want to do.
So I tried with virtual DOM and rendering function, replacing slot by <v-elements-generator :elements="$store.state.help.helpElements" /> inside my VHelp comp.
The store helpElements is a simple array with objects inside.
{
type: 'a',
config: {
class: 'btn btn-default',
},
nestedElements: [
{
type: 'span',
value: 'example',
},
{
type: 'i',
},
],
},
Then inside my VElementsGenerator comp I have a render function that with render element inside virtual DOM from an object like
<script>
import {
cloneDeep,
isEmpty,
} from 'lodash';
export default {
name: 'VElementsGenerator',
props: {
elements: {
type: Array,
required: true,
},
},
methods: {
iterateThroughObject(object, createElement, isNestedElement = false) {
const generatedElement = [];
for (const entry of object) {
const nestedElements = [];
let elementConfig = {};
if (typeof entry.config !== 'undefined') {
elementConfig = cloneDeep(entry.config);
}
if (entry.nestedElements) {
nestedElements.push(this.iterateThroughObject(entry.nestedElements, createElement, true));
}
generatedElement.push(createElement(
entry.type,
isEmpty(elementConfig) ? entry.value : elementConfig,
nestedElements
));
if (typeof entry.parentValue !== 'undefined') {
generatedElement.push(entry.parentValue);
}
}
if (isNestedElement) {
return generatedElement.length === 1 ? generatedElement[0] : generatedElement;
}
return createElement('div', generatedElement);
},
},
render(createElement) {
if (this.elements) {
return this.iterateThroughObject(this.elements, createElement);
}
return false;
},
};
</script>
This second method is working well but if I want to render complex data, the object used inside the rendering function is very very long and complex to read.
So I'm trying to find another way to add content to a global component used inside a base layout only when I want it on a child component.
I can't use this VHelp component directly inside children comps because the HTML page architecture will be totally wrong.
I'm wondering if this is possible to add content (preferably HTML) to a component slot from a single file comp without re-creating a new instance of the component?
Furthermore I think this is very ugly to save HTML as string inside a Vuex store. So I don't even know if this is possible and if I need to completely change the way I'm trying to do this.
Any ideas ?

In the store, you should only store data and not an HTML structure. The way to go with this problem would be to store the current state of the content of the v-help component in the store. Then, you would have a single v-help component with a slot (like you already proposed). You should pass different contents according to the state in the store. Here is an abstract example:
<v-help>
<content-one v-if="$store.state.content === 'CONTENT_ONE' />
<content-two v-else-if="$store.state.content === 'CONTENT_TWO' />
<content-fallback v-else />
</v-help>
Child element somewhere else:
<div>
<button #click="$store.commit('setContentToOne')">Content 1</button>
</div>
Vuex Store:
state: {
content: null
},
mutations: {
setContentToOne(state) {
state.content = 'CONTENT_ONE';
}
}
Of course it depends on your requirements and especially on how many different scenarios are used if this is the best way to achieve this. If I understood you correctly, you are saving help elements to the store. You could also save an array of currently selected help elements in there and just display them directly in the v-help component.
EDIT:
Of course you can also just save the static component (or its name) in the store. Then, you could dynamically decide in the child components, which content is shown in v-help. Here is an example:
<v-help>
<component :is="$store.state.helpComponent" v-if="$store.state.helpComponent !== null" />
</v-help>
Test Component:
<template>
test component
</template>
<script>
export default {
name: 'test-component'
};
</script>
Child element somewhere else (variant 1, storing the name in Vuex):
<div>
<button #click="$store.commit('setHelpComponent', 'test-component')">Set v-help component to 'test-component'</button>
</div>
Child element somewhere else (variant 2, storing the whole component in Vuex):
<template>
<button #click="$store.commit('setHelpComponent', testComponent)">Set v-help component to testComponent (imported)</button>
</template>
<script>
import TestComponent from '#/components/TestComponent';
export default {
name: 'some-child-component',
computed: {
testComponent() {
return TestComponent;
}
}
};
</script>
Child element somewhere else (variant 3, storing the name, derived from the imported component, in Vuex; I would go with this variant):
<template>
<button #click="$store.commit('setHelpComponent', testComponentName)">Set v-help component to 'test-component'</button>
</template>
<script>
import TestComponent from '#/components/TestComponent';
export default {
name: 'some-child-component',
computed: {
testComponentName() {
return TestComponent.name;
}
}
};
</script>
Vuex Store:
state: {
helpComponent: null
},
mutations: {
setHelpComponent(state, value) {
state.helpComponent = value;
}
}
See also the documentation for dynamic components (<component :is=""> syntax): https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components

Related

Access scoped slot from component

I got this component:
<template>
<Popover v-slot="{ open }">
<PopoverButton>
{{ title }}
</PopoverButton>
<div v-if="computedOpen">
<PopoverPanel static>
<slot name="popover"></slot>
</PopoverPanel>
</div>
</Popover>
</template>
<script>
import {Popover, PopoverButton, PopoverPanel} from '#headlessui/vue'
import {computed, ref, watch} from 'vue'
import {useRoute} from 'vue-router'
export default {
name: 'DropdownMenuButton',
mixins: [slots],
props: {
name: {
type: String,
},
title: {
type: String,
default: ''
},
},
components: {
Popover,
PopoverButton,
PopoverPanel,
ChevronDownIcon,
},
setup(props) {
const isOpen = ref(null);
const route = useRoute()
watch(route, () => {
isOpen.value = false;
});
const computedOpen = computed(() => {
let open = ...? //this is missing...
return isOpen.value && open.value;
});
return {
computedOpen
}
},
}
</script>
This component makes use of headless UI's popover.
Now I'd like to close the popover once the route changes. While the route-change is being detected fine, I can not access the <Popover>'s open value in the setup() method to determine, whether computedOpen should return true or false.
My question: How can I access v-slot="{ open } in the computed value?
What you want is not possible.
Think about it:
Everything inside <Popover> element (the slot content) is compiled by Vue to a function returning the virtual DOM representation of the content
This function is passed to the Popover component as slots.default and when the Popover component is rendering, it calls that function passing the open value as an argument
So open value is Popover component's internal state and is only accessible inside the template (ie. slot render function)
So I think your best bet is throw away the computed idea and just use the open value directly in the template or use method instead, passing open as an argument
<div v-if="isOpen && open">
<PopoverPanel static>
<slot name="popover"></slot>
</PopoverPanel>
</div>
<div v-if="isPanelOpen(open)">
<PopoverPanel static>
<slot name="popover"></slot>
</PopoverPanel>
</div>

Vue.js : Passing Props Down to Child Components to Update Styles in the DOM

I'm a beginner trying to get my app to pass props that set CSS styles down a chain to child components. I have a listener that checks for view port size, and as the window gets resized, it checks past a certain point and then swaps the css class and passes it down the chain..
I think I may be doing something incorrectly because my child components don't seem to be receiving the new styles and aren't updating in the DOM as I drag the window.
Here is my code.. I removed irrelevant code to make it easier to read:
Page_Listings.vue
<template>
<main>
<section>
<ListingRack
:prp_classes="rackClass"
/>
</section>
</main>
</template>
<script>
import ListingRack from './Listing__Rack.vue';
export default {
name: 'Front_Page__Panel',
data() {
return {
viewportWidth: window.innerWidth
}
},
methods: {},
mounted() { window.onresize = () => this.viewportWidth = window.innerWidth },
components: {ListingRack},
},
computed: {
rackClass: function(){
let theValue;
console.log('>> viewport width is now: ',this.viewportWidth)
if(this.viewportWidth > 1200) {
theValue = "grid_view";
console.log('>> grid view')
}
else {
theValue = 'card_view';
console.log('>> card view')
}
return theValue
}
}
}
</script>
Listing__Rack.vue
<template>
<div class="listing_rack" :class="classes">
<ul>
<li v-for="item in listings" :key="item.postId">
// I removed irrelevant code for hte sake of simplicity in this example.
// listings is a GraphQL returned array of data that generates a list of "listings".
<Listing
:prp_classes=classes
/>
</li>
</ul>
</div>
</template>
<script>
import Listing from './Listing.vue'
export default {
name: 'listing__rack',
data() {
return {
posts: [], // what we get from the database.
listings: [], // what we copy from the database.
classes: this.prp_classes
}
},
props: {
prp_classes: String
},
components: {
Listing
},
watch: {
classes: function(){
//just to check if we're receiving anything...
console.log(">> [Listing_Rack](watch)(classes) there was a change to classes!");
}
}
}
</script>
Listing.vue
<template>
<div :id=id
:class=classes
class="listing"
:style="backgroundStyle"
>
</div>
</template>
<script>
export default {
name: 'Listing',
data() {
return {
classes: this.prp_classes,
backgroundStyle: String
}
},
props: {
prp_classes: String
},
methods: {
checkClasses: function(){
if(this.classes === 'grid_view') this.backgroundStyle = 'background: center / cover no-repeat url(background.jpg);';
}
},
mounted: function() {
this.checkClasses();
},
watch: {
classes: function(){
this.checkClasses();
}
}
}
</script>
My console.logs on rackClass so I know the class swapping part is working, but all my subsequent child components don't seem to be updating accordingly..
Can someone tell me what I'm doing wrong? Is there a better way to do this? How come my props aren't being passed when I drag the window, and how can I dynamically set styles in the DOM?
Your code does not work because of the one big mistake (don't worry, many people do it)
You are passing your classes using props to child components. But instead of using this prop (prp_classes) directly in the child's template, you create an absolutely unnecessary classes property in the data()
Problem with that is that data() is executed only once when the component is created. If the value of the prp_classes prop changes later, classes property from the data() just holds the old value.
To fix this, remove unnecessary classes from the data and use the prop directly in the template...
...bit more explanation by example what is going on:
let prp_classes = 'card_view'
let classes = prp_classes
prp_classes = 'grid_view'
// prp_classes === 'grid_view', classes === 'card_view', prp_classes !== classes
// strings/numbers/Date ...all work the same
let o1 = { a: 1 }
let o2 = o1
o1.a = 2
// o1.a === 2, o2.a === 2, o1 === o2
More to study

Building a form with Vuejs. Pass data from children to parent

I'm trying to build a form using "v-for" for input component and then generate a pdf file with PDFMake using data from inputs. But I didn't know how to pass the data from input component back to parent.
I read a lot of topics, but can't find a way to do this.
Here is short code without additional inputs, checkboxes etc. I plan to use around 15 inputs with different parameters to generate final PDF. Some of parameters also will be used to change final data depending of conditional statements.
Everything is work fine if code in one file, without loop and components. But not now.
Here is parent:
<template lang="pug">
.form
Input(v-for="data in form.client_info" v-bind:key="data.id" v-bind:data="data")
button(#click="pdfgen") Download PDF
</template>
<script>
export default {
components: {
Input: () => import('#/components/items/form/input')
},
data() {
return {
client_name: '',
client_email: '',
form: {
client_info: [
{id:'client_name', title:'Name'},
{id:'client_email', title: 'Email'},
{id:'foo', title: 'foo'}
],
}
}
},
methods: {
pdfgen: function () {
var pdfMake = require('pdfmake/build/pdfmake.js')
if (pdfMake.vfs == undefined) {
var pdfFonts = require('pdfmake/build/vfs_fonts.js')
pdfMake.vfs = pdfFonts.pdfMake.vfs;
}
if (this.foo) {
var foo = [
'Foo: ' + this.foo
];
} else {
foo = ''
]
}
var docDefinition = {
content: [
'Name: ' + this.client_name,
'Email: ' + this.client_email,
'\n',
foo
]
}
pdfMake.createPdf(docDefinition).download('Demo.pdf');
}
}
}
</script>
Here is a children (Input component):
<template lang="pug">
label.form_item
span.form_item_title {{ data.title }}
input.form_item_input(:v-model="data.id" type="text")
</template>
<script>
export default {
props: ['data']
}
</script>
Any ideas how to make it work?
You'll want to use a method that vue has build-in named $emit().
Before going into how to do that, a quick explanation. Because vue attempts to make data flow one-directional there is not a super quick way to just pass data back to a parent. What Vue proposes instead is to pass a method to the child component that, when called, will 'emit' the value it changed to it's parent and the parent can then do what it wants with that value.
So, in your parent component you'll want to add a method that will handle a change when the child emits. This could look something like:
onChildValueChanged(value){ this.someValue = value }
The value we passed to the function will be coming from our child component. We will need to define in our child component what this function should do. In your child component you could have a function that looks like so:
emitValueChange(event){ this.$emit('childFunctionCall', this.someChildValue) }
Next we need to tie those two functions together by adding an attribute on our child template. In this example that might look like:
<Child :parentData="someData" v-on:childFunctionCall="onChildValueChanged"></Child>
What that above template is doing is saying that when the function on:childFunctionCall is 'emited' then our function in the parent scope should fire.
Finally, in the child template we just need to add some event that calls out emiter. That could look like:
<button v-on:click="emitToParent">This is a button</button>
So when our button is clicked, the emiter is called. This triggers the function in our child component named 'emitToParent' which in turn calls the function we passed to our child component.
You'll have to tailor your use case to match the exam
I found a solution using Vuex.
So now my components look like this.
Here is parent:
<template lang="pug">
.form
Input(v-for="data in formClient" v-bind:key="data.id" v-bind:data="data")
button(#click="pdfgen") Download PDF
</template>
<script>
export default {
components: {
Input: () => import('#/components/items/form/input'),
store: () => import('#/store'),
},
computed: {
formClient() { return this.$store.getters.client }
}
}
</script>
Here is a children (Input component):
<template lang="pug">
label.form_item
span.form_item_title {{ data.title }}
input.form_item_input(v-model="data.value" :type="data.input_type")
</template>
<script>
export default {
props: ['data'],
computed: {
form: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
}
</script>
Here is a store module:
<script>
export default {
actions: {},
mutations: {},
state: {
form: {
client: [
{id:'client_name', title:'Name', value: ''},
{id:'client_email', title: 'Email', value: ''},
{id:'foo', title: 'foo', value: ''}
]
}
},
getters: {
client: state => {
return state.form.client;
}
}
}
</script>
Now I can read updated data from store directly from PDFMake function.

Vue JS - Problem with computed property not updating

I am quite new with VueJS and I have been having trouble lately with some computed properties which do not update as I would like. I've done quite some research on Stack Overflow, Vue documentation and other ressources but i haven't found any solution yet.
The "app" is basic. I've got a parent component (Laundry) which has 3 child components (LaundryMachine). The idea is to have for each machine a button which displays its availability and updates the latter when clicked on.
In order to store the availability of all machines, I have a data in the parent component (availabilities) which is an array of booleans. Each element corresponds to a machine's availability.
When I click on the button, I know the array availibities updates correctly thanks to the console.log. However, for each machine, the computed property "available" does not update is I would want it to and I have no clue why.
Here is the code
Parent component:
<div id="machines">
<laundry-machine
name="AA"
v-bind:machineNum="0"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="BB"
v-bind:machineNum="1"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="CC"
v-bind:machineNum="2"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
</div>
</div>
</template>
<script>
import LaundryMachine from './LaundryMachine.vue';
export default {
name: 'Laundry',
components: {
'laundry-machine': LaundryMachine
},
data: function() {
return {
availabilities: [true, true, true]
};
},
methods: {
editAvailabilities(index) {
this.availabilities[index] = !this.availabilities[index];
console.log(this.availabilities);
}
}
};
</script>
Child component:
<template>
<div class="about">
<h2>{{ name }}</h2>
<img src="../assets/washing_machine.png" /><br />
<v-btn color="primary" v-on:click="changeAvailability">
{{ this.availability }}</v-btn>
</div>
</template>
<script>
export default {
name: 'LaundryMachine',
props: {
name: String,
machineNum: Number,
availableArray: Array
},
methods: {
changeAvailability: function(event) {
this.$emit('change-avlb', this.machineNum);
console.log(this.availableArray);
console.log('available' + this.available);
}
},
computed: {
available: function() {
return this.availableArray[this.machineNum];
},
availability: function() {
if (this.available) {
return 'disponible';
} else {
return 'indisponible';
}
}
}
};
</script>
Anyway, thanks in advance !
Your problem comes not from the computed properties in the children, rather from the editAvailabilities method in the parent.
The problem is this line in particular:
this.availabilities[index] = !this.availabilities[index];
As you can read here, Vue has problems tracking changes when you modify an array by index.
Instead, you should do:
this.$set(this.availabilities, index, !this.availabilities[index]);
To switch the value at that index and let Vue track that change.

Vue.js Component rendering after prop update

In Vue.js i have a component (Answer Component) like this:
<template>
<a class="quiz-input-choice" :class="{'quiz-input-choice--selected': answer.selected}"
#click="toggleSelect()" :selected="answer.selected">
<img :src="answer.image_path"/>
<p class="quiz-input-choice__description">{{answer.title}}</p>
</a>
</template>
<script>
export default {
props: ['answer'],
methods: {
toggleSelect() {
this.$parent.$emit('answer-selected', this.answer.id);
}
}
}
</script>
If in the parent (Question Component) I update the "selected" attribute of the element, this component will not be rerendered.
export default {
props: ['question'],
components: {QuizAnswer},
created: function () {
let _self = this;
this.$on('answer-selected', id => {
let i = _self.question.answers.map(item => item.id).indexOf(id);
let answer = _self.question.answers[i];
answer.selected = !answer.selected;
});
}
}
In Vue Developer Console, i checked that Answer component data are updated, so the answer is marked as selected. Anyway, is not rendered with the "quiz-input-choice--selected" class.
If, strangely, I update from the parent other attribute of the prop (for example (answer.title), then the child component is rendered correctly with also the class "quiz-input-choice--selected".
So i guess it's a problem of detecting changes from the child.
Thank you everybody for the answers.
I discovered the problem. The "selected" attribute of the answer was not present in the initial object, so Vue cannot make reactive that attribute.
https://v2.vuejs.org/v2/guide/reactivity.html
I solved making reactive that property in the parent component.
created() {
let self = this;
this.question.answers.forEach(function (answer) {
self.$set(answer, 'selected', false);
});
},
I think you have a structural issue here. You shouldn't submit an event to your parent, since the component is supposed to be self-contained.
What you can do however is emitting an event in the child component (Answer) that will be catch in the parent (Question).
Answer.vue
<template>
<a class="quiz-input-choice" :class="{'quiz-input-choice--selected': answer.selected}"
#click="toggleSelect()" :selected="answer.selected">
<img :src="answer.image_path"/>
<p class="quiz-input-choice__description">{{answer.title}}</p>
</a>
</template>
<script>
export default {
props: ['answer'],
methods: {
toggleSelect() {
this.$emit('answer-selected');
}
}
}
</script>
Question.vue
Your template will have be catching the event like this (I don't know where your answers are so I assume that you have a answers array) :
<answer
v-for="(answer, index) in answers"
:answer="answer"
#answer-selected="answerSelected(index)"
></answer>
And your script will look like this :
export default {
props: ['question'],
components: {QuizAnswer},
data() {
return {
answers: [],
selectedAnswer: -1,
};
},
watch: {
selectedAnswer(newIndex, oldIndex) {
if (oldIndex > -1 && this.answers.length > oldIndex) {
// Reset old value
this.answers[oldIndex].selected = false;
}
if (newIndex > -1 && this.answers.length > newIndex) {
// Set new value
this.answers[newIndex].selected = true;
}
},
},
methods: {
answerSelected(index) {
this.selectedAnswer = index;
},
},
};

Categories

Resources