Scope of Vue Template Syntax Expressions - vue.js

In my Vue.js app, a child components emits an event in the following format:
this.$emit('done-editing', payload)
My parent component is designed in the following manner:
<child-component
v-on:done-editing="console.log(data)">
</child-component>
But when I execute this code, It throws an error saying
TypeError: Cannot read property 'log' of undefined
What I understood was the console object was not found in this scope. (It is originally defined on the window object). I want to know that what is the scope of JavaScript expressions inside v-on:event="…" and how to use console.log inside Vue template syntax.
I know I can do the same thing as below. But is there a way to do it inside a template expression?
<template>
<child-component
v-on:done-editing="logMethod(data)">
</child-component>
</template>
<script>
methods : {
logMethod(data) {
console.log(data)
}
}
</script>

Each handler in a v-on directive is "bound to this". That means, when you try to do:
v-on:some-event="console.log('test')"
You're actually doing:
this.console.log('test')
Which is not valid because this points to the Vue component instance. That's why you can do this:
v-on:some-event="someHandler"
…
methods: {
someHandler() { … }
}
Because the expression inside the v-on directive is automatically prefixed with this. It calls this.someHandler which exists. The same goes for expressions in v-bind directives. Specifically, in the documentation:
[...] all Vue handler functions and expressions are strictly bound to the ViewModel that’s handling the current view [...]
And:
These expressions will be evaluated as JavaScript in the data scope of the owner Vue instance.

I wrote a Vue plugin so you can use $window and $document in your templates.
https://www.npmjs.com/package/window-plugin
This will work after you install the plugin:
<child-component
v-on:done-editing="$window.console.log(data)">
</child-component>

Related

Vuejs:Why is render method returning dynamic `<component>` giving errors?

I am trying to return vue's builtin <component/> from the render method, but it is not recognizing the component. what could be the possible mistake?
Vue.component('comp1',{
template:'<h1>Component1</h1>'
});
Vue.component('comp2',{
template:'<h1>Component2</h1>'
});
let app=new Vue({
el:'#app',
data:function(){
return {
comp:'comp1'
}
},
mounted:function(){
setInterval(()=>{
if(this.comp=='comp1'){
this.comp="comp2"
}else{
this.comp="comp1"
}
},1000);
},
render (h) {
return h('component', this.comp)
}
})
linkto js bin: https://jsbin.com/wakivet/edit?html,js,console,output
Replace:
render(h){
return h('component',this.comp);
}
with
template: '<component :is="comp" />'
There is no built-in component called component in Vue.
Vue needed a syntax for creating dynamic components in a template. It also needed a way to solve the problems caused by in-DOM templates requiring specific elements as children of other elements. Both of these problems could be solved the same way, with the introduction of the is attribute.
But even though the desired tag name is specified via the is attribute we still need to include a tag name in the template. Any tag will work, but the convention is to use the tag <component>.
<component :is="comp">
Using this convention allows other developers to know that the tag is dynamic. It also makes it easier for the Vue template compiler to optimise the template.
Vue will log a warning if anyone tries to register a global component called component, just like it would for other reserved tag names. This helps to ensure there's no ambiguity when those tags are used.
However, it is just a convention. Using other tags will work just fine and with in-DOM templates it is sometimes necessary, see https://v2.vuejs.org/v2/guide/components.html#DOM-Template-Parsing-Caveats.
But what about render functions?
The first argument passed to the function h is the tag name. We don't need to use tricks like the is attribute to make that dynamic, we can just pass it directly:
h(this.comp)
In fact, if you write a template containing <component :is="comp"> then Vue will compile it down to something very similar to this. The compiled render function doesn't keep the is attribute, it throws that away and just passes its value to the h function.
You can see this for yourself if you inspect a compiled template. For example, you can use the tool at https://v2.vuejs.org/v2/guide/render-function.html#Template-Compilation to see how templates are compiled to render functions. The function _c is what you called h.
So to reiterate, there is no such component as <component>. It is merely a placeholder in template syntax for some other component. When you're in a render function you should just use the desired component directly.

Vue.js: property or method is not defined on the instance, but it is

I have a Vue.js component that looks like this:
<!-- MyComponent.vue -->
<script>
export default {
render: () {
return;
},
methods: {
foo() {
alert('hi');
},
},
};
</script>
And then my HTML looks like this:
<my-component #click="foo" />
When I run this I get an error:
Property or method "foo" 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.
I can't seem to understand what I'm doing wrong here -- all the other SO questions about this error seem to be caused by scoping issues, but I'm just dealing with a simple component.
foo would need to be defined in the component using my-component, not in my-component itself.
Also, you'd need to do #click.native, unless you're specifically $emiting an event called click in my-component.
If you were to use #click="foo" inside of this component on an html element it would work like you expect (a #click on a component needs .native).

Vue2 Custom Directive modifying component call

I am attempting to write a Vue directive that updates the properties of a component before the component is evaluated.
For example, consider the following.
<b-modal v-i18n:title="'i18n.key'">
Hello
</b-modal>
b-modal is a Vue Component and it takes a property called 'title'. I would like to have a custom directive that can set the property title after translating the supplied key.
That is, I would like the above code to get rewritten by the directive to:
<b-modal title="Translated Text">
Hello
</b-modal>
So far I have read the following resources and found no reference on how to do this.
https://css-tricks.com/power-custom-directives-vue/
http://optimizely.github.io/vuejs.org/guide/directives.html
My current attempt looks like this:
Vue.directive('i18n', {
inserted: function (el,binding) {
const i18nKey = binding.value;
const attrName = binding.arg;
el.setAttribute(attrName, i18nKey);
}
})
This attempt sadly falls short. It results in a change to the final DOM element and has no affect on the property being past to the Vue component.
How can I can the above directive be modified to change the properties being past to the b-modal component?

Access v-for model used within Vue 2 custom directive

Question
How can I update the model data used within a Vue 2 custom directive?
Setup:
<ul>
<li v-for="item in items">
<select my-directive="item">...</select>
</li>
</ul>
So let's say I have a directive with a hook like this:
Vue.directive('chosenjs', {
inserted: function (el, binding, vnode) {
// Here, I'm setting up a callback with the jQuery Chosen library, but this could be any callback.
jQuery(el).chosen().change(function(event, change) {
// CODE HERE...
});
}
});
In the CODE HERE... section, if binding.value is a pointer (array/object), then this is straight-forward. For example, for an array, I'd do e.g. binding.value.push(someValue), and Vue's observable will handle it. But if the value is a primitive, then what can be done?
If the directive is not used within a v-for loop, you can use the vnode to modify the data in the component. It works great as I show here.
But if it is in a v-for, it seems there's no way. Even with access to the binding.expression, there's no way to get at the v-for item.
Background
I'm trying to work with Vue and the ChosenJS jQuery library. I got most of the way there with this answer, but there is a bug when the directive is used within a v-for loop.

Processing local components with vue

I have the following code:
<my-messages>
<message>Hello</message>
<message>World</message>
</my-messages>
For now, I did my <my-messages> component be renderized as:
<div class="Messages">
<!-- slot here -->
</div>
And I like to do the same for <message>, but the problem is that I receiving the error Unknown custom element: <message>. Except if I change my code to:
<my-messages inline-template>
<message>Hello</message>
</my-messages>
It seems a bit hack, once that I should declare the [inline-template] to all <my-messages> components, instead of it be treated directly from this component as a default rule (eg. an option as inlineTemplate: true should do the work, if it exists).
The expected render should be like:
<div class="Messages">
<div class="message">Hello</div>
<div class="message">World</div>
</div>
My component currently:
export default {
components: {
// Should process <message> sub-component.
message: require('./Messages.Message.vue'),
}
}
Edit: on reality, the inline-template seems mixing both <div>s from template, and not nesting it.
inline-template is not a hack. I think The problem is you're not registering message components at the same place where you're using my-messages component.
So the parent component that has my-messages as a child can't understand message, you need to register it in the parent too, when you use inline-template the scope changes and and whatever is inside will be treated as inner content. You can find it in the docs
EDIT
There isn't a way to have <message> to be only usable as a child of <my-messages>, you could however throw an exception if it's misused
mounted() {
if (!this.$parent.$el.classList.contains('my-message')) {
this.$destroy();
throw new Error('You must wrap the message in a my-message');
}
}
Note that this supposes that the class my-message is available in the root element, this way you can use any wrapper element.