Only show slot if it has content, when slot has no name? - vue.js

As answered here, we can check if a slot has content or not. But I am using a slot which has no name:
<template>
<div id="map" v-if="!isValueNull">
<div id="map-key">{{ name }}</div>
<div id="map-value">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
name: {type: String, default: null}
},
computed: {
isValueNull() {
console.log(this.$slots)
return false;
}
}
}
</script>
I am using like this:
<my-map name="someName">{{someValue}}</my-map>
How can I not show the component when it has no value?

All slots have a name. If you don't give it a name explicitly then it'll be called default.
So you can check for $slots.default.
A word of caution though. $slots is not reactive, so when it changes it won't invalidate any computed properties that use it. However, it will trigger a re-rendering of the component, so if you use it directly in the template or via a method it should work fine.
Here's an example to illustrate that the caching of computed properties is not invalidated when the slot's contents change.
const child = {
template: `
<div>
<div>computedHasSlotContent: {{ computedHasSlotContent }}</div>
<div>methodHasSlotContent: {{ methodHasSlotContent() }}</div>
<slot></slot>
</div>
`,
computed: {
computedHasSlotContent () {
return !!this.$slots.default
}
},
methods: {
methodHasSlotContent () {
return !!this.$slots.default
}
}
}
new Vue({
components: {
child
},
el: '#app',
data () {
return {
show: true
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<button #click="show = !show">Toggle</button>
<child>
<p v-if="show">Child text</p>
</child>
</div>

Why you dont pass that value as prop to map component.
<my-map :someValue="someValue" name="someName">{{someValue}}</my-map>
and in my-map add prop:
props: {
someValue:{default: null},
},
So now you just check if someValue is null:
<div id="map" v-if="!someValue">
...
</div

Related

Why can't you add a Vue.js reactive property directly to root model?

Here is some code that uses $set() to add a new reactive prop to the model. It works fine.
<template>
<div id="app">
<div>
Prop1: {{ x.prop1 }}
</div>
<div>
<input type="button" value="Go" #click="go()">
</div>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
x: {}
};
},
methods: {
go() {
this.$set(this.x, 'prop1', 'yay');
}
}
};
</script>
Now, if I remove the x root property and try to add prop1 directly to the this it doesn't work.
<template>
<div id="app">
<div>
Prop1: {{ prop1 }}
</div>
<div>
<input type="button" value="Go" #click="go()">
</div>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
};
},
methods: {
go() {
this.$set(this, 'prop1', 'yay');
}
}
};
</script>
I get that you should do this kind of thing, but I can't figure out why it doesn't work.
As stated in the docs:
The target object cannot be a Vue instance, or the root data object of a Vue instance.
It's a technical limitation.

Component with optional value prop

I'm writing a re-usable component. It's basically a section with a header and body, where if you click the header, the body will expand/collapse.
I want to allow the consumer of the component to use v-model to bind a boolean to it so that it can expand/collapse under any condition it wants, but within my component, the user can click to expand/collapse.
I've got it working, but it requires the user of the component to use v-model, if they don't, then the component doesn't work.
I essentially want the consumer to decide if they care about being able to see/change the state of the component or not. If they don't, they shouldn't have to supply a v-model attribute to the component.
Here's a simplified version of my component:
<template>
<div>
<div #click="$emit('input', !value)">
<div>
<slot name="header">Header</slot>
</div>
</div>
<div :class="{ collapse: !value }">
<div class="row">
<div class="col-xs-12">
<div>
<slot></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-property-decorator";
#Component
export default class CollapsibleSection extends Vue {
#Prop({ default: true }) public value: boolean;
}
</script>
Update:
I've come up with a solution that meets my requirements functionally. It's a little more verbose than I would like, so if anyone has a more terse solution, I would love to read about it, and I will gladly mark it as the accepted answer if it meets my requirements with less code/markup.
<template>
<div>
<div #click="toggle">
<div>
<slot name="header">Header</slot>
</div>
</div>
<div :class="{ collapse: !currentValue }">
<div class="row">
<div class="col-xs-12">
<div>
<slot></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-property-decorator";
#Component
export default class CollapsibleSection extends Vue {
#Prop({ default: true }) public value: boolean;
public currentValue = true;
public toggle() {
this.currentValue = !this.currentValue;
this.$emit('input', this.currentValue);
}
public mounted() {
this.currentValue = this.value;
}
#Watch('value')
public valueChanged() {
this.currentValue = this.value;
}
}
</script>
Your update works and has the right gist in general, but instead of a watcher it would be better to use a computed property. See the docs for computed properties and watchers for more info.
I've excluded the class notation in the below snippet to have it runnable on-site.
Vue.component('expandable', {
props: {
value: {
// Just to be explicit, not required
default: undefined,
validator(value) {
return typeof value === 'boolean' || typeof value === 'undefined';
},
},
},
template: `
<div class="expandable">
<p #click="toggle()">toggle</p>
<slot v-if="isOpen" />
</div>
`,
data() {
return {
internalValue: true,
};
},
computed: {
isOpen() {
return (typeof this.value !== 'undefined') ? this.value : this.internalValue;
},
},
methods: {
toggle() {
this.internalValue = !this.internalValue;
this.$emit('input', !this.isOpen);
}
}
});
new Vue({
el: '#app',
data() {
return {
isOpen: false,
}
}
})
.expandable {
border: 2px solid blue;
margin-bottom: 1rem;
}
<script src="https://unpkg.com/vue"></script>
<div id="app">
<expandable>
<p>no model</p>
</expandable>
<expandable v-model="isOpen">
<p>has model</p>
</expandable>
</div>

How do i get the ViewModel from Vue's :is

i have these components:
<template id="test-button-component">
<div class="test-button__container">
This is test button
<button #click="clickButton">{{buttonTitle}}</button>
</div>
</template>
<template id="test-button-component2">
<div class="test-button__container">
<button></button>
</div>
</template>
I try to use the Vue's :is binding to do a component binding by name as follow:
<div :is='myComponentName' ></div>
every time the myComponentName changed to other component, the new component will replace the old component. The thing i need is, is there any way i can get the instance of the component so i can get the view model instance of the currently bound component?
You can add a ref attribute (for example ref="custom") to the <div> tag for the dynamic component. And then reference the component instance via this.$refs.custom.
Here's a simple example where the data of the component gets logged whenever the value being bound to the is prop is changed:
new Vue({
el: '#app',
data() {
return {
value: 'foo',
children: {
foo: {
name: 'foo',
template: '<div>foo</div>',
data() {
return { value: 1 };
}
},
bar: {
name: 'bar',
template: '<div>bar</div>',
data() {
return { value: 2 };
}
}
}
}
},
computed: {
custom() {
return this.children[this.value];
}
},
watch: {
custom() {
this.$nextTick(() => {
console.log(this.$refs.custom.$data)
});
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<select v-model="value">
<option>foo</option>
<option>bar</option>
</select>
<div :is="custom" ref="custom"></div>
</div>
Note that the $data for the component reference by $refs.custom is getting logged inside of a $nextTick handler. This is because the bound component won't update until the parent view has re-rendered.

Move elements passed into a component using a slot

I'm just starting out with VueJS and I was trying to port over a simple jQuery read more plugin I had.
I've got everything working except I don't know how to get access to the contents of the slot. What I would like to do is move some elements passed into the slot to right above the div.readmore__wrapper.
Can this be done simply in the template, or am I going to have to do it some other way?
Here's my component so far...
<template>
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>
<script>
export default {
name: 'read-more',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
}
</script>
You can certainly do what you describe. Manipulating the DOM in a component is typically done in the mounted hook. If you expect the content of the slot to be updated at some point, you might need to do the same thing in the updated hook, although in playing with it, simply having some interpolated content change didn't require it.
new Vue({
el: '#app',
components: {
readMore: {
template: '#read-more-template',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
mounted() {
const readmoreEl = this.$el.querySelector('.readmore__wrapper');
const firstEl = readmoreEl.querySelector('*');
this.$el.insertBefore(firstEl, readmoreEl);
}
}
}
});
.readmore__wrapper {
display: none;
}
.readmore__wrapper.active {
display: block;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id='app'>
Hi there.
<read-more>
<div>First div inside</div>
<div>Another div of content</div>
</read-more>
</div>
<template id="read-more-template">
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>

Reusable nested VueJS components

Is it possible to declare a component inside another component in Vue.JS?
this is what i'm trying to do:
<!-- this is declared inside some-component.vue file -->
<script>
export default {
components:{
'cmptest' : {
template:'#cmptest',
props:['mprop']
}
},
data : () => ({
val:'world'
})
};
</script>
<template>
<div>
<template id="cmptest">
{{mprop}}
</template>
<cmptest mprop="hello"></cmptest>
<cmptest :mprop="val"></cmptest>
</div>
</template>
I'd like to avoid globally registering the child component if possible (with Vue.component(...))
In other words, I'd like to specify child's <template> inside the parent component file (without doing a huge line template:'entire-html-of-child-component-here')
Sure.
Like this:
https://jsfiddle.net/wostex/63t082p2/7/
<div id="app">
<app-child myprop="You"></app-child>
<app-child myprop="Me"></app-child>
<app-child myprop="World"></app-child>
</div>
<script type="text/x-template" id="app-child2">
<span style="color: red">{{ text }}</span>
</script>
<script type="text/x-template" id="app-child">
<div>{{ childData }} {{ myprop }} <app-child2 text="Again"></app-child2></div>
</script>
new Vue({
el: '#app',
components: {
'app-child': {
template: '#app-child',
props: ['myprop'],
data: function() {
return {
childData: 'Hello'
}
},
components: {
'app-child2': {
template: '#app-child2',
props: ['text']
}
}
}
}
});