How to access content of slot in another child component - javascript

Following problem:
I have a Vue.js component which relies on its parent's DOM. But the moment the prop gets passed, it (this.$el) is undefined, probably because it's not yet mounted then.
My component's vue template file looks like this:
<template>
<md-card>
<md-card-content>
<ol>
<li v-for="item in headings(content)">
<a :href="`#${item.id}`">{{ item.name }}</a>
</li>
</ol>
</md-card-content>
</md-card>
</template>
<script>
export default {
props: ['content'],
methods: {
headings(content) {
// DOM element is used
// At this moment, `content` is undefined
},
},
};
</script>
The component that uses the one above includes this piece of code:
<article-index :content="this.$el"></article-index>
I thought of waiting for the parent component to be mounted, but that way I can't seem to keep the template like above, because it would always try to access the method (or variable) instantly.
How can I solve this?
Edit:
<template>
<div class="content">
<div class="left"><article-index :content="this.$el"></article-index></div>
<div class="article"><slot></slot></div>
<div class="right"><slot name="aside"></slot></div>
</div>
</template>
Here's the parent component's template. The only thing I actually need is the .article div, or the slot's contents.

You can get it using this.$slots, in the parent component's mount function you can access this.$slots and assign it to some variable which can be passed to article-index component.
Following code prints the passed slots:
Vue.component('wrapper', {
name: 'Wrapper',
template: `<div><slot></slot></div>`,
mounted () {
this.$slots.default.forEach(vnode => {
console.log(vnode)
})
}
})
Sample fiddle here.

With the help of #saurabh I was able to find out that I can access the slot I'm passing to the child directly.
But the core problem remained: The component was not mounted at that moment.
So I changed how I'm accessing the passed slot.
Instead of the parent element, I'm now passing the default slot in the parent component.
Since the slots prop is an Array of VNode objects, I cannot use any DOM methods on them. But since a VNode's elm property contains the actual DOM element, I'm using that instead.
Again, the problem: it's not mounted yet.
That's why the v-for now points to the headings data, not the method, which removed.
Instead, I added a mounted() method, which automatically gets called by Vue when the components got mounted.
When that method gets called, the slot has been mounted, so I can access their elm properties. In my case, there are multiple default slots, so the slots array has more than one items. To make it possible to call a specific querySelectorAll, I've added some functional Array magic.
Edit: Since it makes more sense to directly access querySelector on the rendered content, I'm now passing the $refs attribute instead of $slots.
Even though I only need $refs.article, if I pass it directly, I'll get undefined. By passing this.$refs as a whole, the child component can access the article ref even if it doesn't exist before mounting.
So this is my new parent component:
<template>
<div class="content">
<div class="left">
<article-index :refs="this.$refs"></article-index>
</div>
<div class="article" ref="article"><slot></slot></div>
<div class="right"><slot name="aside"></slot></div>
</div>
</template>
and the child:
<template>
<md-card>
<md-card-content>
<ol>
<li v-for="item in headings">
<a #click="scroll(item.id)" :href="hash">
{{ item.name }}
</a>
</li>
</ol>
</md-card-content>
</md-card>
</template>
<script>
import dashify from 'dashify';
export default {
props: ['refs'],
data: () => ({
headings: {},
hash: location.hash,
}),
methods: {
scroll(to) {
this.refs.article.querySelector(`#${to}`).scrollIntoView();
},
},
mounted() {
const elements = Array.from(this.refs.article.querySelectorAll('h2'));
elements.forEach(node => node.id = dashify(node.innerText));
this.headings = elements.map(node => ({
name: node.innerText,
id: node.id,
}));
},
};
</script>

Related

Vue doesnt update component on dynamic variable condition change

I am working with Vuejs. I want to render components based on value of variable val.
My component looks like this
<template v-if="this.$val===1">
<component1 />
</template>
<template v-if="this.$val===2">
<component2 />
</template>
I have defined a global variable val using Vue.prototype and I am updating it using onclick function,where I am changing value of val to 2 but after clicking it doesnt show component2 instead of component 1.
Define val globally in main.js using following line of code
Vue.prototype.$val = 1;
Can someone please help me with this. Thanks
td,dr; Vue.prototypeis not reactive.
I'm going to enumerate issues as I observe them, hoping you'll find them useful.
You're not specifying which version of Vue you're using. Since you're using Vue.prototype, I'm going to guess you're using Vue 2.
Never use this in a <template>.
Inside templates, this is implicit (sometimes formulated: "inside templates this doesn't exist"). What would be this.stuff in controller, is stuff in the template.
You can't conditionally swap the top level <template> of a Vue component. You need to take the conditional either one level up or one level down:
one level up would be: you create separate components, one for each template; declare them and have the v-if in their parent component, rendering one, or the other
one level down would be: you move the v-if inside the top level <template> tag of the component. Example:
<template><!-- top level can't have `v-if` -->
<div v-if="val === 1">
val is 1
<input v-model="val">
</div>
<div v-else>
val is not 1
<input v-model="val">
</div>
</template>
<script>
export default {
data: () => ({ val: 1 })
}
</script>
Note <template> tags don't render an actual tag. They're just virtual containers which help you logically organise/group their contents, but what gets rendered is only their contents.1 So I could have written the above as:
<template><!-- top level can't have v-if -->
<template v-if="val === 1">
<div>
val is 1
<input v-model="val">
</div>
</template>
<template v-else>
<template>
<template>
<div>
val is not 1
<input v-model="val">
</div>
</template>
</template>
</template>
</template>
And get the exact same DOM output.
For obvious reasons, <template> tags become useful when you're working with HTML structures needing to meet particular parent/child constraints (e.g: ul + li, tr + td, tbody + tr, etc...).
They're also useful when combining v-if with v-for, since you can't place both on a single element (Vue needs to know which structural directive has priority, since applying them in different order could produce different results).
Working example with what you're trying to achieve:
Vue.prototype.$state = Vue.observable({ foo: true })
Vue.component('component_1', {
template: `
<div>
This is <code>component_1</code>.
<pre v-text="$state"/>
<button #click="$state.foo = false">Switch</button>
</div>
`})
Vue.component('component_2', {
template: `
<div>
This is <code>component_2</code>.
<pre v-text="$state"/>
<button #click="$state.foo = true">Switch back</button>
</div>
`})
new Vue({
el: '#app'
})
<script src="https://unpkg.com/vue#2.7.10/dist/vue.min.js"></script>
<div id="app">
<component_1 v-if="$state.foo"></component_1>
<component_2 v-else></component_2>
</div>
Notes:
<div id="app">...</div> acts as <template> for the app instance (which is, also, a Vue component)
Technically, I could have written that template as:
<div id="app">
<template v-if="$state.foo">
<component_1 />
</template>
<template v-else>
<component_2 />
</template>
</div>
, which is pretty close to what you were trying. But it would be slightly more verbose than what I used, without any benefit.
I'm using a Vue.observable()2 for $state because you can't re-assign a Vue global. I mean, you can, but the change will only affect Vue instances created after the change, not the ones already created (including current one). In other words, Vue.prototype is not reactive. This, most likely, answers your question.
To get past the problem, I placed a reactive object on Vue.prototype, which can be updated without being replaced: $state.
1 - there might be an exception to this rule: when you place text nodes inside a <template>, a <div> wrapper might be created to hold the text node(s). This behaviour might not be consistent across Vue versions.
2 - Vue.Observable() was added in 2.6.0. It's a stand-alone export of Vue's reactivity module (like a component's data(), but without the component). In v3.x Vue.Observable() was renamed Vue.reactive(), to avoid confusion/conflation with rxjs's Observable.
global variables are accessed in template without this keyword which means $val===1 will work.
Solution 1:
<template>
<component1 v-if='$val === 1' />
<component2 v-else/>
</template>
This will work.
But you could make use of dynamic components in your case.
Solution 2:
<template>
<component :is='currentComponent'/>
</template>
<script>
\\imports of your components will go here
export default {
name: 'app',
components: {
component1, component2
},
computed:{
currentComponent(){
return this.$val === 1?component1:component2;
}
}
}
</script>
Dynamic components are more performant and helps you maintain state of component.

Angular ngOnDestroy didn't fire

Demo
Demo fixed accordingly to accepted answer
Consider a component, let's call it <simple-dialog>, with this template:
<button type="button" (click)="visible = !visible">TOGGLE</button>
<div *ngIf="visible">
<ng-content select="[main]"></ng-content>
</div>
I omit the component TypeScript definition cause it's basically the same as the one generated by ng-cli.
Then I use it like this:
<simple-dialog>
<div main>
<app-form></app-form>
</div>
</simple-dialog>
When i first click the button the child component is rendered; if I click the button again the child component is removed from the DOM.
The problem is that, at this point, app-form's ngOnDestroy is not called.
I'm new to angular, so I am not sure whether my expectation is wrong.
What you are trying to achieve is called conditional content projection.
In your case, by using ng-content, the components are instantiated by the outer component, unconditionally - the inner component just decides not to display it.
If you want conditional instantiation, you should pass a template instead:
<simple-dialog>
<ng-template>
<div main>
<app-form></app-form>
</div>
</ng-template>
</simple-dialog>
and use an #ContentChild annotated property to access
the template from the content within the SimpleDialogComponent:
#ContentChild(TemplateRef, { static: false })
content!: TemplateRef<unknown>;
which can then be rendered in the template as
<div *ngIf="visible">
<ng-container [ngTemplateOutlet]="content"></ng-container>
</div>
You can also read about this here:
https://angular.io/guide/content-projection#conditional-content-projection

How to pass html template as props to Vue component

I have a textarea component that include html tag and I want to get html in edit mode in this component. I use Laravel to generate html.
<template>
<div>
<textarea
:value="content"
:name="name"
:id="id">
<slot></slot>
</textarea>
</div>
</template>
In blade page I used to this component:
<my-component>
<p class="textbox">hello world</p>
</my-component>
when I put this component in page show me tag <slot></slot> in textarea. What should I do? Do you have any solution for my need?
thanks
<textarea> components are treated as static by the Vue renderer, thus after they are put into the DOM, they don't change at all (so that's why if you inspect the DOM you'll see <slot></slot> inside your <textarea>).
But even it if they did change, that wouldn't help much. Just because HTML elements inside <textarea>s don't become their value. You have to set the value property of the TextArea element to make it work.
Anyway, don't despair. It is doable, all you need to overcome the issues above is to bring a small helper component into play.
There are many possible ways to achieve this, two shown below. They differ basically in how you would want your original component's template to be.
Solution: change <textarea> into <textarea-slot> component
Your component's template would now become:
<template>
<div>
<textarea-slot
v-model="myContent"
:name="name"
:id="id">
<slot></slot>
</textarea-slot>
</div>
</template>
As you can see, nothing but replacing <textarea> with <textarea-slot> changed. This is enough to overcome the static treatment Vue gives to <textarea>. The full implementation of <textarea-slot> is in the demo below.
Alternative solution: keep <textarea> but get <slot>'s HTML via <vnode-to-html> component
The solution is to create a helper component (named vnode-to-html below) that would convert your slot's VNodes into HTML strings. You could then set such HTML strings as the value of your <textarea>. Your component's template would now become:
<template>
<div>
<vnode-to-html :vnode="$slots.default" #html="valForMyTextArea = $event" />
<textarea
:value="valForMyTextArea"
:name="name"
:id="id">
</textarea>
</div>
</template>
In both alternatives...
The usage of the my-component stays the same:
<my-component>
<p class="textbox">hello world</p>
</my-component>
Full working demo:
Vue.component('my-component', {
props: ["content", "name", "id"],
template: `
<div>
<textarea-slot
v-model="myContent"
:name="name"
:id="id">
<slot></slot>
</textarea-slot>
<vnode-to-html :vnode="$slots.default" #html="valueForMyTextArea = $event" />
<textarea
:value="valueForMyTextArea"
:name="name"
:id="id">
</textarea>
</div>
`,
data() { return {valueForMyTextArea: '', myContent: null} }
});
Vue.component('textarea-slot', {
props: ["value", "name", "id"],
render: function(createElement) {
return createElement("textarea",
{attrs: {id: this.$props.id, name: this.$props.name}, on: {...this.$listeners, input: (e) => this.$emit('input', e.target.value)}, domProps: {"value": this.$props.value}},
[createElement("template", {ref: "slotHtmlRef"}, this.$slots.default)]
);
},
data() { return {defaultSlotHtml: null} },
mounted() {
this.$emit('input', [...this.$refs.slotHtmlRef.childNodes].map(n => n.outerHTML).join('\n'))
}
});
Vue.component('vnode-to-html', {
props: ['vnode'],
render(createElement) {
return createElement("template", [this.vnode]);
},
mounted() {
this.$emit('html', [...this.$el.childNodes].map(n => n.outerHTML).join('\n'));
}
});
new Vue({
el: '#app'
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<my-component>
<p class="textbox">hell
o world1</p>
<p class="textbox">hello world2</p>
</my-component>
</div>
Breakdown:
Vue parses the <slot>s into VNodes and makes them available in the this.$slots.SLOTNAME property. The default slot, naturally, goes in this.$slots.default.
So, in runtime, you have available to you what has been passed via <slot> (as VNodes in this.$slots.default). The challenge now becomes how to convert those VNodes to HTML String? This is a complicated, still open, issue, which may get a different solution in the future, but, even if it ever does, it will most likely take a while.
Both solutions above (template-slot and vnode-to-html) use Vue's render function to render the VNodes to the DOM, then picks up the rendered HTML.
Since the supplied slots may have arbitrary HTML, we render the VNodes into an HTML Template Element, which doesn't execute any <script> tags.
The difference between the two solutions is just how they "handle back" the HTML generated from the render function.
The vnode-to-html returns as an event that should be picked up by the parent (my-component) which uses the passed value to set a data property that will be set as :value of the textarea.
The textarea-slot declares itself a <textarea>, to the parent doesn't have to. It is a cleaner solution, but requires more care because you have to specify which properties you want to pass down to the <textarea> created inside textarea-slot.
Wrapping up and off-the-shelf alternatives
However possible, it is important to know that Vue, when parsing the declared <template> into <slot>s, will strip some formatting information, like whitespaces between top-level components. Similarly, it strips <script> tags (because they are unsafe). These are caveats inherent to any solutions using <slot>s (presented here or not). So be aware.
Typical rich text editors for Vue, work around this problem altogether by using v-model (or value) attributes to pass the code into the components.
Well known examples include:
vue-ace-editor: Demo/codepen here.
Vue Prism Editor: Demo here.
vue-monaco (the code editor that powers VS Code): demo here.
vue-codemirror: Demo here. This is by far the most starred on github.
They all have very good documentation in their websites (linked above), so it would be of little use for me to repeat them here, but just as an example, see how codemirror uses the value prop to pass the code:
<codemirror ref="myCm"
:value="code"
:options="cmOptions"
#ready="onCmReady"
#focus="onCmFocus"
#input="onCmCodeChange">
</codemirror>
So that's how they do it. Of course, if <slot>s - with its caveats - fit your use case, they can be used as well.
The short answer is NOT POSSIBLE
Your slot is put inside an textarea tag. Textare tag is only able to display the text content on its box.
So in the case you want a kind of "HTML edit mode", you may looking for an WYSIWYG editor, I recommend you can use CKEditor for VueJS, the editor even will allow you to direct edit HTML code
https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/frameworks/vuejs.html
Your HTML
<div id="app">
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig"></ckeditor>
</div>
Your Component
const app = new Vue( {
el: '#app',
data: {
editor: ClassicEditor,
editorData: '<p>Editable Content HTML</p>',
editorConfig: {
// The configuration of the editor.
}
}
} );
In your case if you want to write your own content editor you can use div with attribute contenteditable="true" rather than textarea. After this you can write your text decoration methods ...
The generated html with laravel store in myhtml and use it in vue component.
Example: I also uploaded to codesandbox [Simple Vue Editor]
<template>
<div>
<button #click="getEditorCotent">Get Content</button>
<button #click="setBold">Bold</button>
<button #click="setItalic">Italic</button>
<button #click="setUnderline">Underline</button>
<button #click="setContent">Clear</button>
<div class="myeditor" ref="myeditor" contenteditable v-html="myhtml" #input="onInput"></div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String
},
data: () => {
return {
myhtml:
"<h1>Simple editor</h1><p style='color:red'>in vue</p><p>Hello world</p>" // from laravel server via axios call
};
},
methods: {
onInput(e) {
// handle user input
// e.target.innerHTML
},
getEditorCotent() {
console.log(this.$refs.myeditor.innerHTML);
},
setBold() {
document.execCommand("bold");
},
setItalic() {
document.execCommand("italic");
},
setUnderline() {
document.execCommand("underline");
},
setContent() {
// that way set your html content
this.myhtml = "<b>You cleared the editor content</b>";
}
// PS. Good luck!
}
};
</script>
<style scoped>
.myeditor {
/* text-align: left; */
border: 2px solid gray;
padding: 5px;
margin: 20px;
width: 100%;
min-height: 50px;
}
</style>

Can I change element tag name with Polymer data binding?

I have this application which contains many types of Polymer elements that can be added to a main Polymer app element. This main element manages instances of these elements and shows them in a UI.
eg.
item1.html
item2.html
item3.html
my-app.html
As I add new types of items (eg. item4.html), I need to make several changes to the main UI to handle creating, managing, and showing them. Each type is unique enough that I do not want to merge them into a single item type.
What I'd like to do is have each Polymer element 'register' itself into my-app by calling a function which can add a new object to an array.
To do this, my-app will have a property called itemMap which is an array of objects. One property in this object is the type of item.
itemMap: [
{
type: 'item-1',
instances: []
}, {
type: 'item-2',
instances: []
}
...
]
This implementation works in code. When adding a new instance, I can add a new object to the instances array for that type. However, I do not know how to show the items in the UI. As each type is a different Polymer element, I cannot use a simple dom-repeat template. At the same time, I do not want to hardcode each type in the main UI to improve modularity.
Right now I have:
<iron-list id="item-1-list" items="[[item1_array]]" as="item" grid>
<template>
<div class="item">
<item-1 properties=[[item]]></item-1>
</div>
</template>
</iron-list>
<iron-list id="item-2-list" items="[[item2_array]]" as="item" grid>
<template>
<div class="item">
<item-2 properties=[[item]]></item-2>
</div>
</template>
</iron-list>
What I want to do is something like the snippet below, which would work for any type I create.
<template is="dom-repeat" items="{{itemMap}}" as="itemType" id="item-grid">
<iron-list id="[[itemType.type]]-list" as="item" grid items="[[itemType.instances]]">
<template>
<div class="item">
<[[itemType.type]] properties=[[item]]></[[itemType.type]]>
</div>
</template>
</iron-list>
</template>
However, this does not work.
Is this possible, or something equivalent, or am I going down the wrong path completely?
Polymer data binding does not work for tag names. What you might do to implement this kind of behavior is to create another custom element that accepts the item type as a property and dynamically creates an element of that type:
<template is="dom-repeat" items="{{itemMap}}" as="itemType" id="item-grid">
<iron-list id="[[itemType.type]]-list" as="item" grid items="[[itemType.instances]]">
<template>
<div class="item">
<x-item-selector type=[[itemType.type]] properties=[[item]]></x-item-selector>
</div>
</template>
</iron-list>
</template>
There could be several ways to implement the x-item-selector element: declarative and imperative:
Declarative: Create a set of <dom-if>s--one per type
If there are only a few element types and you can list them all, you could create a template for the x-item-selector like below:
<template>
<template is="dom-if" if="[[_isEqual(type, 'item-1')]]" restamp>
<item-1 properties="[[properties]]"></item-1>
</template>
...
</template>
Imperative: Observe the type property and update the child element manually
If you are going to support many types of elements, you might want to avoid a large set of <dom-if>s, and update the children of the x-item-selector element imperatively whenever the type property changes. As a downside, the mapping for the properties property you'll also have to establish manually.
_onTypeChanged(newType, oldType) {
if (newType !== oldType) {
for (let child of this.children) {
this.removeChild(child);
}
const newChild = document.createElement(newType);
newChild.properties = this.properties;
// also need to add some code to update newChild.properties
// when this.properties change
this.appendChild(newChild);
}
}

Vue v-on:click does not work on component

I'm trying to use the on click directive inside a component but it does not seem to work. When I click the component nothings happens when I should get a 'test clicked' in the console. I don't see any errors in the console, so I don't know what am I doing wrong.
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>vuetest</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
App.vue
<template>
<div id="app">
<test v-on:click="testFunction"></test>
</div>
</template>
<script>
import Test from './components/Test'
export default {
name: 'app',
methods: {
testFunction: function (event) {
console.log('test clicked')
}
},
components: {
Test
}
}
</script>
Test.vue (the component)
<template>
<div>
click here
</div>
</template>
<script>
export default {
name: 'test',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
If you want to listen to a native event on the root element of a component, you have to use the .native modifier for v-on, like following:
<template>
<div id="app">
<test v-on:click.native="testFunction"></test>
</div>
</template>
or in shorthand, as suggested in comment, you can as well do:
<template>
<div id="app">
<test #click.native="testFunction"></test>
</div>
</template>
Reference to read more about native event
I think the $emit function works better for what I think you're asking for. It keeps your component separated from the Vue instance so that it is reusable in many contexts.
// Child component
<template>
<div id="app">
<test #click="$emit('test-click')"></test>
</div>
</template>
Use it in HTML
// Parent component
<test #test-click="testFunction">
It's the #Neps' answer but with details.
Note: #Saurabh's answer is more suitable if you don't want to modify your component or don't have access to it.
Why can't #click just work?
Components are complicated. One component can be a small fancy button wrapper, and another one can be an entire table with bunch of logic inside. Vue doesn't know what exactly you expect when bind v-model or use v-on so all of that should be processed by component's creator.
How to handle click event
According to Vue docs, $emit passes events to parent. Example from docs:
Main file
<blog-post
#enlarge-text="onEnlargeText"
/>
Component
<button #click="$emit('enlarge-text')">
Enlarge text
</button>
(# is the v-on shorthand)
Component handles native click event and emits parent's #enlarge-text="..."
enlarge-text can be replaced with click to make it look like we're handling a native click event:
<blog-post
#click="onEnlargeText"
></blog-post>
<button #click="$emit('click')">
Enlarge text
</button>
But that's not all. $emit allows to pass a specific value with an event. In the case of native click, the value is MouseEvent (JS event that has nothing to do with Vue).
Vue stores that event in a $event variable. So, it'd the best to emit $event with an event to create the impression of native event usage:
<button v-on:click="$emit('click', $event)">
Enlarge text
</button>
As mentioned by Chris Fritz (Vue.js Core Team Emeriti) in VueCONF US 2019
If we had Kia enter .native and then the root element of the base input changed from an input to a label suddenly this component is broken and it's not obvious and in fact, you might not even catch it right away unless you have a really good test. Instead by avoiding the use of the .native modifier which I currently consider an anti-pattern, and will be removed in Vue 3, you'll be able to explicitly define that the parent might care about which element listeners are added to...
With Vue 2
Using $listeners:
So, if you are using Vue 2, a better option to resolve this issue would be to use a fully transparent wrapper logic. For this, Vue provides a $listeners property containing an object of listeners being used on the component. For example:
{
focus: function (event) { /* ... */ }
input: function (value) { /* ... */ },
}
and then we just need to add v-on="$listeners" to the test component like:
Test.vue (child component)
<template>
<div v-on="$listeners">
click here
</div>
</template>
Now the <test> component is a fully transparent wrapper, meaning it can be used exactly like a normal <div> element: all the listeners will work, without the .native modifier.
Demo:
Vue.component('test', {
template: `
<div class="child" v-on="$listeners">
Click here
</div>`
})
new Vue({
el: "#myApp",
data: {},
methods: {
testFunction: function(event) {
console.log('test clicked')
}
}
})
div.child{border:5px dotted orange; padding:20px;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="myApp">
<test #click="testFunction"></test>
</div>
Using $emit method:
We can also use the $emit method for this purpose, which helps us to listen to a child component's events in the parent component. For this, we first need to emit a custom event from a child component, like:
Test.vue (child component)
<test #click="$emit('my-event')"></test>
Important: Always use kebab-case for event names. For more information and a demo regading this point please check out this answer: VueJS passing computed value from component to parent.
Now, we just need to listen to this emitted custom event in the parent component, like:
App.vue
<test #my-event="testFunction"></test>
So basically, instead of v-on:click or the shorthand #click we will simply use v-on:my-event or just #my-event.
Demo:
Vue.component('test', {
template: `
<div class="child" #click="$emit('my-event')">
Click here
</div>`
})
new Vue({
el: "#myApp",
data: {},
methods: {
testFunction: function(event) {
console.log('test clicked')
}
}
})
div.child{border:5px dotted orange; padding:20px;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="myApp">
<test #my-event="testFunction"></test>
</div>
With Vue 3
Using v-bind="$attrs":
Vue 3 is going to make our life much easier in many ways. One example is that it will help us create a simpler transparent wrapper with less config, by just using v-bind="$attrs". By using this on child components, not only will our listener work directly from the parent, but also any other attributes will also work just like they would with a normal <div>.
So, with respect to this question, we will not need to update anything in Vue 3 and your code will still work fine, as <div> is the root element here and it will automatically listen to all child events.
Demo #1:
const { createApp } = Vue;
const Test = {
template: `
<div class="child">
Click here
</div>`
};
const App = {
components: { Test },
setup() {
const testFunction = event => {
console.log("test clicked");
};
return { testFunction };
}
};
createApp(App).mount("#myApp");
div.child{border:5px dotted orange; padding:20px;}
<script src="//unpkg.com/vue#next"></script>
<div id="myApp">
<test v-on:click="testFunction"></test>
</div>
But, for complex components with nested elements where we need to apply attributes and events to the <input /> instead of the parent label we can simply use v-bind="$attrs"
Demo #2:
const { createApp } = Vue;
const BaseInput = {
props: ['label', 'value'],
template: `
<label>
{{ label }}
<input v-bind="$attrs">
</label>`
};
const App = {
components: { BaseInput },
setup() {
const search = event => {
console.clear();
console.log("Searching...", event.target.value);
};
return { search };
}
};
createApp(App).mount("#myApp");
input{padding:8px;}
<script src="//unpkg.com/vue#next"></script>
<div id="myApp">
<base-input
label="Search: "
placeholder="Search"
#keyup="search">
</base-input><br/>
</div>
A bit verbose but this is how I do it:
#click="$emit('click', $event)"
UPDATE: Example added by #sparkyspider
<div-container #click="doSomething"></div-container>
In div-container component...
<template>
<div #click="$emit('click', $event);">The inner div</div>
</template>
Native events of components aren't directly accessible from parent elements. Instead you should try v-on:click.native="testFunction", or you can emit an event from Test component as well. Like v-on:click="$emit('click')".
One use case of using #click.native is when you create a custom component and you want to listen to click event on the custom component. For example:
#CustomComponent.vue
<div>
<span>This is a custom component</span>
</div>
#App.vue
<custom-component #click.native="onClick"></custom-component>
#click.native always work for this situation.
App.vue
<div id="app">
<test #itemClicked="testFunction($event)"/>
</div>
Test.vue
<div #click="$emit('itemClicked', data)">
click here
</div>
From the documentation:
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
In my case i stumbled on this problem when migrating from Angular to VUE. Fix was quite easy, but really difficult to find:
setValue(index) {
Vue.set(this.arr, index, !this.arr[index]);
this.$forceUpdate(); // Needed to force view rerendering
}

Categories