How to get element calling function in Vue (no event) - vue.js

I have this code and I need to have the calling div on the getWidth function. Is this possible? Since it's not an event I'm not sure how this can be handled. Generally I'd do $event but it doesn't exist in this context.
<div :style="{width: getWidth($this_element)}">
</div>
This is contained in a v-for loop.

The current DOM element is already available in getWidth() via this.$el. If you wanted to access the element's width in JavaScript, you could do something like this:
<template>
<div :style="{width: getWidth()}">...</div>
</template>
<script>
export default {
methods: {
getWidth() {
return (this.$el.clientWidth * .33) + 'px';
}
}
}
</script>
new Vue({
el: '#app',
methods: {
getWidth() {
return (this.$el.clientWidth * .33) + 'px';
}
}
})
.my-el {
display: inline-block;
background: #eee;
}
<script src="https://unpkg.com/vue#2.5.17"></script>
<div id="app">
<div class="my-el" :style="{width: getWidth()}">{{getWidth()}}</div>
</div>

Related

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>

vue component to use common functions

I'm looking to get a handle on the Vue CLI3 project system. Currently refactoring a long single html file of in-line vue into real '.vue' components. One goal is to use some common functions among my vue components for various things.
In my common-functions.js file I've got something like this:
function capitalize(str) {
return str[0].toUpperCase() + str.substr(1, );
};
And in my HelloWorld.vue file I've got this and it's not working through many various attempts. All searches I find seem to be dealing with other things, surely there's an easy way to just use some common functions, right??
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<ul>
<li v-for='c in categoryNames'>{{ c }}</li>
</ul>
</div>
</template>
<script>
require('../js/common-functions.js');
export default {
name: 'HelloWorld',
data () {
return {
msg: capitalize('welcome to Your Vue.js App!'),
categoryNames : this.$root.categoryNames
}
}
}
</script>
Of course the message is:
[Vue warn]: Error in data(): "ReferenceError: capitalize is not defined"
found in
---> <HelloWorld> at src/components/HelloWorld.vue
<App> at src/App.vue
<Root>
At the end of common-functions.js, export the function:
export default capitalize;
And in the HelloWorld.vue, import it with:
import capitalize from '../js/common-functions.js';
// this should replace the require line
One Solution:
Register your global functions to Vue.prototype by Vue.use().
Like below demo:
let myGlobalAPIGroup1 = { // API Group 1
install: function (_Vue) {
if(!_Vue.prototype.$apiGroup1) {
_Vue.prototype.$apiGroup1 = {}
}
_Vue.prototype.$apiGroup1.capitalize = function (str) {
return str[0].toUpperCase() + str.substr(1, );
}
}
}
let myGlobalAPIGroup2 = { // API Group 2
install: function (_Vue) {
if(!_Vue.prototype.$apiGroup2) {
_Vue.prototype.$apiGroup2 = {}
}
_Vue.prototype.$apiGroup2.capitalize = function (str) {
return str[0].toUpperCase() + str.substr(1, ) + '#';
}
}
}
Vue.use(myGlobalAPIGroup1) //register
Vue.use(myGlobalAPIGroup2) //register
new Vue({
el: '#app',
data() {
return {
testValues: ['label a', 'label b'],
}
},
methods:{
testAPI1: function(item) {
return this.$apiGroup1.capitalize(item)
},
testAPI2: function(item) {
return this.$apiGroup2.capitalize(item)
}
}
})
#app > div {
display: inline-block;
margin-left: 5px;
border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
<h3>Group 1:</h3>
<p v-for="(item, index) in testValues" :key="index">{{testAPI1(item)}}</p>
</div>
<div>
<h3>Group 2:</h3>
<p v-for="(item, index) in testValues" :key="index">{{testAPI2(item)}}</p>
</div>
</div>

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>

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>

Pass data from child to parent in Vuejs (is it so complicated?)

I have read about it:
vuejs update parent data from child component
https://forum.vuejs.org/t/passing-data-back-to-parent/1201/2
The concept is the same, I need to pass a data object from child to parent. I have used $emit to pass data to parent component but it doesn't works. Do you know what is wrong? You can check my code here:
Vue.component('list-products', {
delimiters: ['[[', ']]'],
template: '#list-products-template',
props: ['products'],
data: function () {
return {
productSelected: {}
}
},
methods: {
showDetailModal: function (product) {
console.log('click product in child, how can i pass this product to productSelected data in parent?');
console.log(product);
this.productSelected = product;
this.$emit('clickedShowDetailModal', product);
}
}
});
var app = new Vue({
delimiters: ['[[', ']]'],
el: '#resultComponent',
data: {
listProducts: [
{'name':'test1',id:1},
{'name':'test2',id:2},
{'name':'test3',id:3}
],
productSelected: {}
},
methods: {
clickedShowDetailModal: function (value) {
console.log('value');
console.log(value);
this.productSelected = value;
}
}
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="resultComponent" data-toggler=".small-up-2" class="row small-up-1">
<list-products :products="listProducts"></list-products>
</div>
<script type="text/x-template" id="list-products-template">
<div>
<div class="column column-block" v-for="(product, index) in products" :product="product" :index="index" :key="product.id">
<li class="more-benefits">
<a #click="showDetailModal(product)">Click me [[ product.name ]] and check console.log ยป</a>
</li>
</div>
</div>
</script>
Props are for parent -> child
You can use $emit for child -> parent
v-on directive captures the child components events that is emitted by $emit
Child component triggers clicked event :
export default {
methods: {
onClickButton (event) {
this.$emit('clicked', 'someValue')
}
}
}
Parent component receive clicked event:
<div>
<child #clicked="onClickChild"></child>
</div>
...
export default {
methods: {
onClickChild (value) {
console.log(value) // someValue
}
}
}
You aren't listening to the event. I changed the event name to clicked-show-detail. Try this.
In the showDetailModal method of your component.
this.$emit('clicked-show-detail', product);
In your Vue.
<list-products :products="listProducts" #clicked-show-detail="clickedShowDetailModal"></list-products>
Example.
Nightmare to find "hello world" example out there for $emit so I added the example below (Minimal lines of code + semantic names of functions).
"Hello world" On click change parent data
Vue.component('child', {
template: `
<div class="child">
<button v-on:click="childMethod">CLICK - child Method pass data from product component</button>
</div>
`,
data: function () {
return {
child_msg: "message from child"
}
},
methods: {
childMethod: function() {
this.$emit('child-method', this.child_msg)
}
}
})
var app = new Vue({
el: '#app',
data: {
msg: "I am the blue parent!!!!!!!!!!!!!!!!!!",
},
methods: {
updateParent(value_from_child) {
this.msg = value_from_child;
alert("hello child" + value_from_child)
}
}
})
.child{ background: gray; padding: 15px; }
button{ cursor: pointer; }
#app{ border: 1px red dashed; padding: 15px; background: lightblue; color: blue;
}
<div id="app">
<p>{{msg}}</p>
<!-- ###### The trick happens her ###### -->
<child class="child" v-on:child-method="updateParent"></child>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.js"></script>
codepen: https://codepen.io/ezra_siton/pen/YzyXNox?editors=1010