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

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>

Related

Property or method "item" is not defined on the instance but referenced during render

The application throws me an error in the console.
The property or method "logo" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option or for class-based components, by initializing the property
This is what HTML and script look like.
<template id="items-template">
<span class="items">
<img v-if="logo.name" src="/images/name.png"/>
</span>
</template>
#section scripts {
<script type="text/javascript">
Vue.component('items', {
props: ['logo'],
template: '#items-template'
});
</script>
}
Here's an example of how it should work:
<div id="app">
<items :logo="logo"></items>
</div>
<script type="text/x-template" id="items-template">
<div>
<span class="items">
<img v-if="logo.name" src="https://thumbs-prod.si-cdn.com/d4e3zqOM5KUq8m0m-AFVxuqa5ZM=/800x600/filters:no_upscale():focal(554x699:555x700)/https://public-media.si-cdn.com/filer/a4/04/a404c799-7118-459a-8de4-89e4a44b124f/img_1317.jpg"/>
</span>
</div>
</script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var itemComponent = {
template: "#items-template",
props: {
logo: Object
}
}
new Vue({
el: "#app",
data: {
logo: {
name: 'someName'
}
},
components: {
'items': itemComponent
}
})
</script>
In this example, div#app is parent, and it passes down logo to items-component logo prop.

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

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>

Dynamically change <style> inside a Single File Component

Is it possible to dynamically change the content of a scoped inside a Single File Component?
You could do it using the v-html directive.
Since I don't know your actual code I will just give you the code for a basic proof of concept.
In the template...
<template>
<div>
<head v-html="styles"></head>
<div class="test">
<p>change this paragraph</p>
</div>
<textarea name="" id="" cols="30" rows="10" v-model="csscode"> </textarea>
</div>
</template>
In the script...
<script>
export default {
data() {
return{
csscode: null,
styles: null,
}
},
watch:{
csscode(val){
this.styles = '<style>' + val + '</style>';
}
}
}
</script>
Inside style tag, no. Its not possible because of build extracts a .css file.
But as an alternative you can use javascript object as an style object.
var app = new Vue({
el: '#app',
data: function(){
return {
textAreaStyle: {border: '2px solid blue', color: 'red', width: '500px', height: '300px'}
}
},
methods: {
updateStyle (event) {
this.$set(this.$data, 'textAreaStyle', JSON.parse(event.target.value));
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<div id="app">
<textarea :style="textAreaStyle" #change="updateStyle">{{textAreaStyle}}</textarea>
</div>

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>