Component with optional value prop - vue.js

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>

Related

VueJS - using v-bind to pass an attribute supplied by a webservice

I started to work recently on a VueJS project (first time with that framework) and I face a problem.
I have an object (called "propObject") defined in a mother component. That propObject gets its value via a webservice, called in a beforeRouteEnter method in that mother component.
I have to pass this propObject to a child component so I can display what's inside (a "libelle" attribute, among other things). I tried to do it using v-bind and props but I didn't manage to make it work.
Here is my code :
Mother.vue
<template>
<div class="row justify-content-center">
<b-container>
<b-row>
{{propObject.libelle}}
<b-col> <cpm-child :prop-object="propObject"/></b-col>
[...]
</b-row>
</b-container>
</template>
<script lang="ts" src="./mother.component.ts"></script>
Mother.component
#Component({
components: {
'cpm-child': Child,
},
})
export default class Mother extends Vue {
#Inject('propObjectService') private propObjectService: () => propObjectService;
public propObject: IPropObjectClass = new PropObjectClass();
beforeRouteEnter(to, from, next) {
next(vm => {
if (to.params.propObjectId) {
vm.load(to.params.propObjectId);
}
});
}
public load(propObjectId: string): void {
this.propObjectService()
.find(propObjectId)
.then(res => {
this.propObject = res;
});
}
}
Child.vue
<template>
<div>
<span>
{{propObject.libelle}}
[...]
</span>
</div>
</template>
<script lang="ts" src="./child.component.ts"></script>
Child.component
export default class Child extends Vue {
props: {
propObject: IPropObjectClass,
}
}
propObject.model.ts
export interface IPropObjectClass {
code?: string;
libelle?: string;
[...]
}
export class PropObjectClass implements IPropObjectClass {
constructor(
public code?: string,
public libelle?: string,
[...]
) {}
}
My goal is to display the {{propObject.libelle}} in the child vue. In the Google Chrome's console, propObject is considered "undefined".
Last information : {{propObject.libelle}} is displayed correctly in the mother vue after a few seconds, so the propObjectService works as intended.
So far, nothing I tried worked, so any help would be greatly appreciated. If you need further clarification, don't hesitate to ask.
I created a sample with Vue 2 / Vue CLI showing a standard way of initializing a prop with data before rendering the child. You should be able to port it to your app.
The main takeaways are that you can call your data service in the parent (Mother) created() lifecycle hook. And by using the v-if directive, you child will not be rendered until the prop has been updated with data from the service call.
Parent.vue
<template>
<div class="parent">
<h4>Parent</h4>
<hr>
<child v-if="user" :userProp="user"/>
</div>
</template>
<script>
import axios from 'axios'
import Child from './Child.vue'
export default {
components: {
Child
},
data() {
return {
user: null
}
},
methods: {
getUser() {
axios.get('https://jsonplaceholder.typicode.com/users/1')
.then(response => this.user = response.data)
.catch(error => console.log(error));
}
},
created() {
this.getUser();
}
}
</script>
Child.vue
<template>
<div class="child">
<h5>Child</h5>
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-3 font-weight-bold">ID</div>
<div class="col-md-5">{{ user.id }}</div>
</div>
<div class="row">
<div class="col-md-3 font-weight-bold">NAME</div>
<div class="col-md-5">{{ user.name }}</div>
</div>
<div class="row">
<div class="col-md-3 font-weight-bold">USER NAME</div>
<div class="col-md-5">{{ user.username }}</div>
</div>
<div class="row">
<div class="col-md-3 font-weight-bold">EMAIL</div>
<div class="col-md-5">{{ user.email }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
userProp: {
type: Object,
required: true
}
},
data() {
return {
user: this.userProp
}
}
}
</script>

Watching properties in child component does not work

I have 2 components, a parent and a child. I want to modify the value of a prop in my parent component (by calling a method when a button is clicked) and send it to the child component. In the child component, I want to watch for changes in my prop, so that anytime it changes, it does something (for testing purposes, I tried to console.log() the prop).
Parent:
<template>
<div>
<h5>Your Feeds</h5>
<div id="feeds">
<div class="card" v-for="feed in feeds">
<div class="card-body" :id="feed['_id']" >
{{ feed['name'] }}
<button v-on:click="loadFeed(feed['_id'])">Button</button>
</div>
</div>
</div>
</div>
</template>
<script>
import GridComponent from "./GridComponent";
export default {
name: "FeedsListComponent",
data() {
return {
feeds: []
}
},
mounted() {
axios
.get("/getFeeds")
.then(response => (this.feeds = response.data))
.catch(error => console.log(error))
},
methods: {
loadFeed(id) {
this.feedId = id
}
},
components: {
GridComponent
}
}
</script>
Child:
<template>
<div id="grid">
<v-grid
theme="compact"
:source="rows"
:columns="columns"
></v-grid>
</div>
</template>
<script>
import VGrid from "#revolist/vue-datagrid";
export default {
name: "Grid",
props: ['feedId'],
data() {
return {
columns: [],
rows: [],
};
},
watch: {
feedId: function(val, oldVal) {
console.log(val)
console.log(oldVal)
console.log(this.feedId)
//here I want to send an ajax request with feedId to one of my controllers in order to get
//the data needed for rows and colums
}
},
components: {
VGrid,
},
};
</script>
I put together a sample that is working in order to help you diagnose why yours isn't working:
Parent.vue
<template>
<div class="parent">
<h3>Parent</h3>
<div class="row">
<div class="col-md-6">
<button class="btn btn-secondary" #click="incrementCounter">Change parent message</button>
</div>
</div>
<child :propMessage="message" />
</div>
</template>
<script>
import Child from '#/components/stackoverflow/watch-prop/Child'
export default {
components: {
Child
},
data() {
return {
counter: 0
}
},
computed: {
message() {
return 'Message' + this.counter;
}
},
methods: {
incrementCounter() {
this.counter++;
}
}
}
</script>
Child.vue
<template>
<div class="child">
<hr>
<div class="row">
<div class="col-md-6">
<label>Message in child from watched prop:</label>{{ dataMessage }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
propMessage: {
type: String,
required: true
}
},
data() {
return {
dataMessage: this.propMessage
}
},
watch: {
propMessage(newMessage) {
this.dataMessage = newMessage;
}
}
}
</script>
<style scoped>
label {
font-weight: bold;
margin-right: 0.5rem;
}
</style>

Only show slot if it has content, when slot has no name?

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

Vuejs How to use $emit function properly?

I have two components SingleTaskItem and ControlArea. ControlArea has a collapse button and when that button is clicked I want to call a function in SingleTaskItem. Here is my code so far. Can you please tell me what am I doing wrong?
SingleTaskItem:
<template>
<div class="SingleTaskItem">
<ControlArea v-bind:collapsed="collapsed"
v-bind:onClickCollapse="onClickCollapse"/>
</div>
</template>
<script>
export default {
name: "SingleTaskItem",
data() {
return {
collapsed: false
};
},
methods: {
onClickCollapse(value) {
console.log("on Click Collapse called");
this.collapsed = value;
}
}
};
</script>
ControlArea:
<template>
<div class="ControlArea">
<div class="action-btn edit">
<i class="fas fa-ellipsis-h"></i>
</div>
<div class="action-btn collapsible">
<i v-if="collapsed" v-on:click="uncollapse" class="fas fa-chevron-down"></i>
<i v-else v-on:click="collapse" class="fas fa-chevron-up"></i>
</div>
</div>
</template>
<script>
export default {
name: "ControlArea",
props: {
collapsed: Boolean
},
methods: {
collapse(event) {
console.log("collapse function is called");
this.$emit("onClickCollapse", "true");
},
uncollapse(event) {
this.$emit("onClickCollapse", "false");
}
}
};
</script>
Instead of v-bind:onClickCollapse="onClickCollapse" you should use v-on:onClickCollapse. This is kind of easy to miss because you used the word 'on' in your event name - it might be clearer to remove that.
Also, to pass that true/false string you need to pass $event into your function call: v-on:onClickCollapse($event). To clean this up you should probably also pass true/false booleans rather than strings.

In Vue, anyway to reference to the element when binding attributes in template?

In template scope, are there variable referencing the element itself like a $this or $el?
Instead of,
<template>
<div #click="$emit('xxx')" :class="{active:mode=='xxx'}" something_for_xxx></div>
<div #click="$emit('yyy')" :class="{active:mode=='yyy'}" something_for_yyy></div>
<div #click="$emit('zzz')" :class="{active:mode=='zzz'}" something_for_zzz></div>
</template>
Can we write something like the following, to avoid forgetting to change one of the mode name?
<template>
<div mode="xxx" #click="$emit($this.mode)" :class="{active:mode==$this.mode}" something_for_xxx></div>
<div mode="yyy" #click="$emit($this.mode)" :class="{active:mode==$this.mode}" something_for_yyy></div>
<div mode="zzz" #click="$emit($this.mode)" :class="{active:mode==$this.mode}" something_for_zzz></div>
</template>
Workaround:
<template>
<div v-for"mode_ in ["xxx"] #click="$emit(mode_)" :class="{active:mode==mode_}" something_for_xxx></div>
<div v-for"mode_ in ["yyy"] #click="$emit(mode_)" :class="{active:mode==mode_}" something_for_yyy></div>
<div v-for"mode_ in ["zzz"] #click="$emit(mode_)" :class="{active:mode==mode_}" something_for_zzz></div>
</template>
In the event handlers, you can always access $event.target to access the element (see https://v2.vuejs.org/v2/guide/events.html#Method-Event-Handlers) but for inline binding (like :class) you cannot because the element has not been rendered yet.
I suggest you change how you cycle through each value
<div v-for="elMode in ['xxx', 'yyy', 'zzz']"
#click="$emit('click', elMode)" :class="{active:mode==elMode}"/>
That is the typical situation where you should build your elements in a v-for loop:
Vue.component('my-component', {
template: '#my-component',
props: {
mode: String,
},
data() {
return {
modes: ['xxx', 'yyy', 'zzz'],
};
},
});
new Vue({
el: '#app',
data: {
mode: 'xxx',
},
methods: {
log(event) {
this.mode = event;
console.log(event);
}
},
});
.active {
color: green;
}
.pointer {
cursor: pointer;
}
<script src="https://unpkg.com/vue#2"></script>
<div id="app">
<my-component
:mode='mode'
#click="log($event)"
></my-component>
</div>
<template id="my-component">
<div>
<div
v-for="currentMode of modes"
#click="$emit('click', currentMode)"
:class="{active:mode==currentMode}"
class="pointer"
>{{currentMode}}</div>
</div>
</template>