Is Passing DOM Nodes through a Vue Prop bad practice - javascript

I am currently in the process of writing a Carousel component and am trying to decide on the best method of implementation.
See the following component:
<template>
<!-- Carousel -->
<div class="carousel-container">
<div ref="carousel" class="carousel">
<slot></slot>
</div>
<carousel-navigation v-if="items.length" :orientation="orientation" :container="$refs.carousel" :items="items"></carousel-navigation>
</div>
</template>
<script>
import CarouselNavigation from 'core-js/components/common/CarouselNavigation';
export default {
components: {
'carousel-navigation': CarouselNavigation
},
props: {
orientation: {
type: String,
required: false,
default: 'horizontal',
validator: (value) => {
return ['horizontal', 'vertical'].indexOf(value) !== -1;
}
}
},
data() {
return {
items: []
}
},
mounted () {
this.items = [...this.$refs.carousel.children];
}
}
</script>
The carousel-navigation component accepts various props, but specifically items and container.
The container prop is expected to be a single DOM node, and the items prop is expected to be an array of DOM nodes.
The reason I need to pass DOM nodes is because the carousel-navigation needs to know where to scroll to (items) and what to scroll (container).
Is this bad practice? If so, what would be a better way to achieve this?

Related

Redraw vue component on fetch update

When on button click I want to refresh list of items.
Button is trigger on a sibling component.
Watch method only gets called once. But I need a constant refresh
Parent element.
<template>
<div class="container">
<Filter #changedKeywords="reloadItems"></Filter>
<List :platforms="platforms" :filters="keywords"></List>
</div>
</template>
<script>
imports...
export default {
name: "Holder",
components: {Filter, List},
methods: {
reloadItems: function (data){
if(data.keywords) {this.keywords = data.keywords};
}
},
data(){
return {
keywords : null,
}
}
}
</script>
I want to redraw child this element multiple times, on each (filter)button click
<template>
<section class="list">
<div class="container">
<div class="holder">
<Game v-for="data in list" :key="data.id" :data="data" />
</div>
</div>
</section>
</template>
<script>
import Game from "./Game";
export default {
name: "List",
props: ['filters', 'platforms'],
components: {Game},
data() {
return{
list: [],
}
},
watch: {
filters: async function() {
console.log('gets called only once!!!'); // this is where I want to fetch new items
const res = await fetch('/api/game/list/9', {
method: 'POST',
body: JSON.stringify({'filters' : this.filters})
});
this.list = await res.json();
}
},
}
</script>
When you're watching objects and arrays you need to use a deep watcher.
The Solution
watch: {
filter: {
deep: true,
async handler(next, previous) {
//your code here
}
}
}
The Reason
Javascript primitives are stored by value, but Objects (including Arrays which are a special kind of Object) are stored by reference. Changing the contents of an Object doesn't change the reference, and the reference is what is being watched. Going from null to some object reference is an observable change, but subsequent changes aren't. When you use a deep watcher it will detect nested changes.

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

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

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

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.

how to resolve - Duplicate presence of slot "default" found in the same render tree

I am getting the below error when using <slot> multiple times:
Duplicate presence of slot "default" found in the same render tree -
this will likely cause render errors.
There are solutions that use "scoped slots" but what I understand is that it's good to use those with v-for. I am not sure, I may be wrong, let me know if I am.
I have a situation where I need to duplicate static content (with markup) in a child component multiple times.
Parent Component:
<template>
<child-comp>
<h1>Lorem Ipusm</h1>
<button #click="fnDoSomething">Yahoo!<button>
<!-- ... there will be a lot more that will go here in the default slot -->
<child-comp>
<template>
Child Component:
<template>
<div>
<h2>Need one default slot here</h2>
<slot><slot>
<div>
<h2>Need one more default slot here</h2>
<slot><slot>
<div>
</div>
<template>
If the above issue can't be fixed or it's a limitation of Vue.js, then please let me know how to clone the slot (or something like that) and have it still be reactive.
Use render function should be able to implement what you need like below demo:
But you may meet some troubles like this link decribes: Why are duplicated slots bad?.
As the developer from Vue.js core team said:
Vue would re-use the same vnode objects (which represent elements)
multiple times during the actual creation of the DOM elements.
The problem with that is that each vnode gets a reference to its
corresponding DOM element set.
If you re-use the same vnode objects multiple times, these references
get overwritten and you end up with DOM elements that have no
representation in the virtual dom, or vnodes that refer to the wrong
element.
So In below demo, when you click the button, you will find the first slot of the first case is not synced (VNode is overwritten).
If your default slot is fully static content then doesn't bind to any properties and methods, it should be OK to use multiple default slots in render(). But if not, you have to use scoped slot to implement what you need.
Or you can deep clone this.$slots.default (check VNode constructor), it will avoid the overwritten issue. (check the third case in below demo)
Vue.config.productionTip = false
Vue.component('child', {
render: function (createElement) {
return createElement(
'div',
[
this.$slots.default, // default slot
createElement('div', {
attrs: {
name: 'test'
},
style: {fontSize: '10px', 'color':'green'}
}, this.$slots.default) // default slot
]
)
}
})
function deepClone(vnodes, createElement){
let clonedProperties = ['text','isComment','componentOptions','elm','context','ns','isStatic','key']
function cloneVNode(vnode) {
let clonedChildren = vnode.children && vnode.children.map(cloneVNode)
let cloned = createElement(vnode.tag, vnode.data, clonedChildren)
clonedProperties.forEach(function(item){
cloned[item] = vnode[item]
})
return cloned
}
return vnodes.map( cloneVNode )
}
Vue.component('child2', {
render: function (createElement) {
return createElement(
'div',
[
this.$slots.default, // default slot
createElement('div', {
attrs: {
name: 'test'
},
style: {fontSize: '10px', 'color':'green'}
}, deepClone(this.$slots.default, createElement) ) // default slot
]
)
}
})
Vue.component('child1', {
render: function (createElement) {
return createElement(
'div',
[
this.$slots.default, // default slot
createElement('div', {
attrs: {
name: 'test'
},
style: {fontSize: '10px', 'color':'green'}
}, this.$slots.my) // default slot
]
)
}
})
new Vue({
el: '#app',
data() {
return {
test: {
'item': 'test',
'prop1': 'a'
}
}
},
methods:{
changeData: function() {
this.test.item='none'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button #click="changeData()">Click me!!!</button>
<h1 style="background-color:red">Use multiple default slot:</h1>
<child><h1>{{test}}</h1></child>
<h1 style="background-color:red">Use scoped slot instead:</h1>
<child1><h1>{{test}}</h1><template slot="my"><h1>{{test}}</h1></template></child1>
<h1 style="background-color:red">Use Deep Clone (Default) instead:</h1>
<child2><h1>{{test}}</h1><template slot="my"><h1>{{test}}</h1></template></child2>
</div>

Categories

Resources