Activate method on router link click in Vue - javascript

I am working on account removal on a Chrome extension and I have the following button:
<button #click="remove" id="button_remove" class="btn btn-default" style="float: right;">Remove Account</button>
JS
methods:{
remove(event){
app.removeAccountData(url,apikey);
}
},
I also have this router-link:
<router-link :to="{ name: 'RemovedWId', params: { accountid: chosenAccount.apikey}}" text="delete_account"><span class="glyphicon glyphicon-trash" aria-hidden="true" style="margin-top: 2px; cursor: pointer;"></span> Remove Account</router-link>
Is there a way to use the router-link with the JS function?

Vue 2
With Vue Router 3, <router-link> is a component wrapper around <a>, so we'll need to use the .native modifier for it to properly receive the event:
<router-link #click.native="removeAccount" to="/remove">Remove</router-link>
demo 1
Vue 3
With Vue Router 4, <router-link> no longer passes its attributes and event handlers to its inner component, and Vue 3 no longer supports the .native event modifier. You'd have to apply the #click handler manually via <router-link>'s custom default slot.
Apply <router-link>'s custom prop to enable the default slot customization.
In the <router-link>'s default slot, add an <a> and bind the following slot props:
a. href (the resolved URL to the route) to <a>.href
b. navigate (the method that performs the navigation to the route) to <a>.#click. Pass navigate and the $event (a special argument) to a method that runs the secondary method and then the navigate() method with $event as the argument.
<router-link to="/remove" custom v-slot="{ href, navigate }">
<a :href="href" #click="wrapNavigate(navigate, $event)">Remove</a>
</router-link>
export default {
methods: {
removeAccount() {
// ...
},
wrapNavigate(navigate, event) {
this.removeAccount()
navigate(event)
},
},
}
demo 2

Related

Vue.js - switching from one component to another

I'm quite new to vue.js.
I have a page that lists details of a list of people, with an 'Edit' button against each person.
When I click the Edit button, I want to switch to another page which shows a form for editing the selected person's details.
I'm not sure what is the best way to do this. I'm using Bootstrap and Router in my solution.
Option 1: I thought it would be straightforward to route to '/person/:id' when clicking the Edit button, but not sure how to do this from the click handler method.
Option 2:
Below is the main component, where I'm trying to switch between the two components 'PersonsList' and 'EditPersonData' upon receiving an event from either. PersonsList is emitting the event successfully, but I'm not sure how to listen to it here and switch to EditPesonData component.
Home.vue
<template>
<div class="home">
<h1 class='home-text'>
{{message}}
</h1>
<component v-bind:is="component"/>
</div>
</template>
<script>
import PersonsList from '#/components/PersonsList'
import EditPersonData from '#/components/EditPersonData'
export default {
name: 'Home',
data() {
return {
message: 'Welcome to Person Details!',
component: "PersonsList"
}
},
components: {
PersonsList,
EditPersonData
}
};
</script>
Any help would be very much appreciated.
Thanks
The best way would be to send the id as a param in the route /person/:id.
To do this, you can append the id of each list item, for example:
<div v-for="(user,index) in userList" :key="index">
<a :href="'/person/' + user.id">Edit</a>
</div>
Then, you would get it in the other component as this.$route.params.id in order to get more details about the user of this id.
Make sure you define the route in the router with the id param as follows: person/:id
UPDATE :
If you want to change the route on a click function:
<b-btn #click="editPerson(user.id)">Edit</b-btn>
And in the methods, add the function as follows:
editPerson(userId){
this.$router.push({
name: 'person',
params: { id: userId }
});
}

How to make router-link work with custom link component?

<base-link> component
I have a Vue component <base-link>, which I use every time I want to have an achor. It's mostly for applying styles specific to links, so that all the links across the whole page look the same without applying global styles.
Make <router-link> use <base-link>
When using <router-link> component to create a link, I cannot apply those styles (<base-link> styles are scoped) unless <router-link> uses my <base-link> component to create the anchor element.
Fortunately <router-link> provides tag attribute, which seems to do exactly that. Unfortunately I can't get it to work. I have 2 problems:
All my components are locally registered (I use ES6 modules with Webpack and import components locally every time I need them). <router-link> doesn't know what <base-link> component is and can't render it. Is there a way to inject a local component for <router-link> to use?
To solve problem #1, I thought it's enough to declare <base-link> component globally. Unfortunately it still doesn't work. This time <base-link> component gets rendered properly, but is still not functional - doesn't react to click events. It seems to me the problem is that it's href attribute isn't set at all. Is there a way to make <router-link> set it properly? (without setting it manually)
Question
How do I solve problems #1 and #2? I suspect #1 might be not possible, but I hope at least #2 is.
Code example
Here is a pen with code below, which illustrates both problems.
const router = new VueRouter({
routes: [
{
path: "/",
component: {
template: '<p>Homepage template</p>'
}
},
{
path: "/subpage",
component: {
template: '<p>Subpage template</p>'
}
}
]
});
// Globally registered BaseLink.
Vue.component('BaseLinkGlobal', {
props: {
href: String
},
template: `
<a
:href="href"
class="BaseLinkGlobal"
>
<slot />
</a>
`
})
const vue = new Vue({
el: "#app",
router,
components: {
// Locally registered BaseLink.
BaseLinkLocal: {
props: {
href: String
},
template: `
<a
:href="href"
class="BaseLinkLocal"
>
<slot />
</a>
`
}
},
template: `
<div>
<!-- 2 router links. One uses locally registered BaseLink
-- and the other one a globally registered one. -->
<nav>
<router-link
to="/"
tag="base-link-local"
>
Home
</router-link>
<router-link
to="/subpage"
tag="base-link-global"
>
Subpage
</router-link>
</nav>
<router-view />
</div>
`
});
You can create a base link component which can double as a normal a tag or <router-link> when you wish.
//Base link
<template>
<component :is="type" :class="{'base': type === 'a'}" v-bind="$attrs">
<slot></slot>
</component>
</template>
<script>
export default {
props: {
routerLink: {
type: Boolean,
default: false
}
},
computed: {
type() {
return this.routerLink ? 'router-link' : 'a'
}
}
}
</script>
<style scopex>
.base {
border: 1px solid red;
}
</style>
Usage
when you want to use it as a normal link just do not provide the router-link prop as below:
<base-link>This is a base a tag</base-link>
To use it as a router-link just add the router-link prop along with the to prop:
<base-link router-link to="/">This is router-link</base-link>
Explanation about the base-link component:
We use a component which is provided by vuejs to render a tag or router-link base on the truthiness of the routerLink prop.
A class of .base is added if it is a normal link i.e a
we bind $attrs which allows us to make the component more transparent i.e allows us to use attributes like href or to without passing them as props.
<base-link href="https://google.com">go to google</base-link>
You can have a look here for more explanation about usage of $attrs
This is for solving problem #2
The global component doesn't inherit the event listener of the router link. You can make it inherit by adding v-on="$listeners" to the global component.
// Globally registered BaseLink.
Vue.component('BaseLinkGlobal', {
props: {
href: String
},
template: `
<a
:href="href"
class="BaseLinkGlobal"
v-on="$listeners"
>
<slot />
</a>
`
})
The link works after adding it: https://codepen.io/jacobgoh101/pen/YvqJxL?editors=0010

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
}

How to access content of slot in another child component

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>

specify target action to a ember component

I have a component that is wrapping content defined by another template. I want an action on the template to trigger a method in my surrounding component. Is this possible?
Here is what I have for my template. Note this is shortened for brevity.
{{#drop-down}}
<div class="menu-selector clickable" {{action "toggleDropdown"}}>
</div>
{{/drop-down}}
This is my component:
DropDownComponent = Ember.Component.extend
showDropdown: false
actions:
toggleDropdown: ->
#toggleProperty 'showDropdown'
`export default DropDownComponent`
I can verify that everything else in my component is working. If I put the action in my component that loads this template, it works fine. But that's not where I want it.
So you would like to send an action to particular components. Take a notice that
A component is a custom HTML tag whose behavior you implement using JavaScript and whose appearance you describe using Handlebars templates. They allow you to create reusable controls that can simplify your application's templates.
and
An Ember.Component is a view that is completely isolated.
You are probably using wrong tool here. You should use instead custom view.
App.DropdownView = Ember.View.extend
showDropdown: false
elemendId: 'dropdown'
actions:
toggleDropdown: ->
#toggleProperty 'showDropdown'
return;
{{#view 'dropdown'}}
<div>
<div class="menu-selector clickable" {{action "toggleDropdown"}}>
</div>
{{/view}}
Then you can send an action to view by
Ember.View.views.dropdown.send('toggleDropdown');
Demo: http://jsbin.com/zoqiluluco/1/

Categories