Conditional link behavior in VueJS - vue.js

Couldn't find a proper name for the title, will be glad if someone figures out a better name.
I have a component which represents a product card. The whole component is wrapped in <router-link> which leads to product page.
However I have another case, when I do not need the component to lead to a product page, but instead I need to do some other action.
The only solution I found is to pass a callback function as a prop, and based on this, do something like:
<router-link v-if="!onClickCallback">
... here goes the whole component template ...
</router-link>
<div v-if="onClickCallback" #click="onClickCallback">
... here again goes the whole component template ...
</div>
How can I do this without copy-pasting the whole component? I tried to do this (real code sample):
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">
However I got this: Invalid handler for event "click": got null
Plus not sure if it's possible to pass prevent modificator for click and this just looks weird, there should be a better architectural solution

Commenting on the error, you could use an empty function instead of null, in the real code snippet
<router-link class="clothing-item-card-preview"
:class="classes"
:style="previewStyle"
:to="{ name: 'clothingItem', params: { id: this.clothingItem.id }}"
v-on="{ click: onClick ? onClick : null }">

This should works (replace a for "router-link" then insert right properties)
Further infos :
https://fr.vuejs.org/v2/guide/components-dynamic-async.html
v-bind is simply an Object where each keys is a props for your component, so here, I programmatically defined an object of properties depending on the wrapper (router link or a simple div). However we cannot do this for events (of course we could create our own event listener but it's a little bit tricky) so I simply but an handle method.
new Vue({
el: "#app",
data: {
products : [{onClickCallback : () => { alert("callback"); return true;}}, {}, {}]
},
methods : {
handleClick(product, event) {
if (!product.onClickCallback) return false
product.onClickCallback()
return true
},
getMyComponentName(product) {
if (product.onClickCallback) return "div"
return "a"
},
getMyComponentProperties(product) {
if (product.onClickCallback) return {is : "div"}
return {
is : "a",
href: "!#"
}
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<component
v-for="(product, index) in products"
:key="index"
v-bind="getMyComponentProperties(product)"
#click="handleClick(product, $event)"
>
<div class="product-card">
<div class="product-card-content">
<span v-show="product.onClickCallback">I'm a callback</span>
<span v-show="!product.onClickCallback">I'm a router link</span>
</div>
</div>
</component>
</div>

Do you have to use a <router-link>? If it can safely be a <div>, you could use something like
<div #click="handleClick" ...>
<!-- component template -->
</div>
and
methods: {
handleClick (event) {
if (this.onClickCallback) {
this.onClickCallback(event)
} else {
this.$router.push({ name: 'clothingItem', ... })
}
}
}
See https://router.vuejs.org/guide/essentials/navigation.html

Related

Vue onEnable/onDisable event

In my vue application I need to observe an element getting enabled/disabled (It binds to a function) and by looking that I need to trigger an onEnabled/onDisabled event which will clean up some other data nodes.
So is there a listener like #click, #enabled or something?
Eg:
<v-checkbox :value="getValue(layout.responseNode)" #change="setValue(layout.responseNode, $event)" :label="expression(layout.label)" :disabled="expression(layout.enableIf)" ></v-checkbox>
This is the code so far with me. here enableIf will be a dynamic expression from server.
Its properly working now.
Now I need to run some more expression like
<v-checkbox :value="getValue(layout.responseNode)" #change="setValue(layout.responseNode, $event)" :label="expression(layout.label)" :disabled="expression(layout.enableIf)" #onDisabled="expression(layout.disableCommand)" ></v-checkbox>
Is there an event matching onDisabled?
i would recommend watchers you can bind a variable/computed to :disabled of the checkbox and watch the value changing
exp.
<template>
<div>
<p>{{ checkboxState }}</p>
<input type="checkbox" :disabled="checkboxState" />
<button #click="checkboxChanged()">Disable Checkbox!</button>
</div>
</template>
<script>
export default {
name: "App",
data: () => {
return {
checkboxState: true,
};
},
methods: {
checkboxChanged() {
this.checkboxState = !this.checkboxState;
},
},
watch: {
checkboxState() {
// this is fired when the checkboxState changes
console.log("fired when checkboxState changes");
},
},
};
</script>
note: the function name and the variable must have the same name for watchers to work.
Like this Sandbox

How to search within nested objects

I have done my research trying to figure out how to achieve what I am describing below, however I had no luck.
In my Algolia index, some records have nested objects.
For example, title and subtitle attributes are of the following format:
title:
{
"en": "English title",
"gr": "Greek title"
}
I would like to execute queries only for a specific subset (in our example "en" or "gr") of these attributes, withoute "exposing" any facet in the UI — language selection would ideally be done “automatically” based on a variable (lang) passed to the Vue component with props. I am using Laravel Scout package with default Vue implementation, as described in documentation here.
My InstantSearch implementation is pretty simple, I am not defining anything specific regarding queries and searchable attributes, I am currently using all the default functionality of Algolia.
<template>
<ais-instant-search
:search-client="searchClient"
index-name="posts_index"
>
<div class="search-box">
<ais-search-box placeholder="Search posts..."></ais-search-box>
</div>
<ais-hits>
<template
slot="item"
slot-scope="{ item }"
>
<div class="list-image">
<img :src="'/images/' + item.image" />
</div>
<div class="list-text">
<h2">
{{ item.title }}
</h2>
<h3>
{{ item.subtitle }}
</h3>
</div>
</template>
</ais-hits>
</ais-instant-search>
</template>
<script>
import algoliasearch from 'algoliasearch/lite';
export default {
data() {
return {
searchClient: algoliasearch(
process.env.ALGOLIA_APP_ID,
process.env.ALGOLIA_SEARCH
),
route: route,
};
},
props: ['lang'],
computed: {
computedItem() {
// computed_item = this.item;
}
}
};
</script>
I would like to somehow pass an option to query “title.en” and “subtitle.en” when variable lang is set to “en”. All this, without the user having to select “title.en” or “subtitle.en” in the UI.
Update
Maybe computed properties is the path to go, however I cannot find how to reference search results/hits attributes (eg item.title) within computed property. It is the code I have commented out.
I think, you can use computed property. Just transform current item according to the current language variable.
new Vue({
template: "<div>{{ computedItem.title }}</div>",
data: {
langFromCookie: "en",
item: {
title: {
en: "Hello",
ru: "Привет"
}
}
},
computed: {
computedItem() {
const item = JSON.parse(JSON.stringify(this.item));
for (value in item) {
if (typeof item[value] === "object" && Object.keys(item[value]).includes(this.langFromCookie))
item[value] = item[value][this.langFromCookie];
}
return item;
}
}
}).$mount("#app")
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
If lang variable is available via props, you can check that inside list-text class and return {{title.en}} or {{title.gr}} accordingly by passing a dynamic lang value title[lang] like below
...
<div class="list-text">
<h2>
{{ item.title[lang] }}
</h2>
<h3>
{{ item.subtitle[lang] }}
</h3>
</div>
If you want to make a request according to lang prop when component mounts ,then you can make a request inside mounted() method then query like below
mounted() {
axios.get(`/getSomethingWithLang/:${this.item.title[this.lang]}`)
...
}

v-on:click problem if the elements has another element inside

I am dealing with the problem of my click event in vue js. I made a click even on element that has another element inside it.
Here's my code:
<span class="pull-down-controller" #click="pullDown($event)">
<span class="indicator">-</span> Controller
</span>
in the frontend it will show - Controller
if I click the word Controller it will call the specified function which is pullDown() but why is it whenever I click the indicator or the minus symbol, it will not do anything even if it is inside the <span> where I put the #click event?
The reason why I put a <span> inside it so I can change the symbol to + using jquery.
thanks!
No need for jQuery, Vue's reactivity provides all you need:
new Vue({
el: '#app',
template: `
<span #click="pullDown" style="font-size: 48px;">
<span>{{ indicator }}</span> Controller
</span>
`,
data () {
return {
expanded: false
}
},
computed: {
indicator () {
return this.expanded ? '+' : '-'
}
},
methods: {
pullDown (event) {
this.expanded = !this.expanded
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

When using conditional rendering, how do I prevent repeating the child components on each condition?

Scenario
I have a custom button component in Vue:
<custom-button type="link">Save</custom-button>
This is its template:
// custom-button.vue
<template>
<a v-if="type === 'link'" :href="href">
<span class="btn-label"><slot></slot></span>
</a>
<button v-else :type="type">
<span class="btn-label"><slot></slot></span>
</button>
</template>
You can see from the template that it has a type prop. If the type is link, instead of the <button> element, I am using <a>.
Question
You'll notice from the template that I repeated the child component, i.e. <span class="btn-label"><slot></slot></span> on both root components. How do I make it so that I won't have to repeat the child components?
In JSX, it's pretty straightforward. I just have to assign the child component to a variable:
const label = <span class="btn-label">{text}</span>
return (type === 'link')
? <a href={href}>{label}</a>
: <button type={type}>{label}</button>
In this situation, I would probably opt to write the render function directly since the template is small (with or without JSX), but if you want to use a template then you can use the <component> component to dynamically choose what you want to render as that element, like this:
Vue.component('custom-button', {
template: '#custom-button',
props: [
'type',
'href',
],
computed: {
props() {
return this.type === 'link'
? { is: 'a', href: this.href }
: { is: 'button', type: this.type };
},
},
});
new Vue({
el: '#app',
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<custom-button type="button">Button</custom-button>
<custom-button type="submit">Submit</custom-button>
<custom-button type="link" href="http://www.google.com">Link</custom-button>
</div>
<template id="custom-button">
<component v-bind="props">
<span class="btn-label"><slot></slot></span>
</component>
</template>
Well you could always create a locally registered component...
// in custom-button.vue
components : {
'label' : {template : '<span class="btn-label"><slot></slot></span>'}
}

Conditional <router-link> in Vue.js dependant on prop value?

Hopefully this is a rather simple question / answer, but I can't find much info in the docs.
Is there a way to enable or disable the anchor generated by <router-link> dependent on whether a prop is passed in or not?
<router-link class="Card__link" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If there's no id passed to this component, I'd like to disable any link being generated.
Is there a way to do this without doubling up the content into a v-if?
Thanks!
Assuming you want to disable anchor tag as in not clickable and look disabled the option is using CSS. isActive should return true by checking prop id.
<router-link class="Card__link" v-bind:class="{ disabled: isActive }" :to="{ name: 'Property', params: { id: id }}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
<style>
.disabled {
pointer-events:none;
opacity:0.6;
}
<style>
If you want to just disable the navigation , you can use a route guard.
beforeEnter: (to, from, next) => {
next(false);
}
If you need to use it often, consider this:
Create new component
<template>
<router-link
v-if="!disabled"
v-bind="$attrs"
>
<slot/>
</router-link>
<span
v-else
v-bind="$attrs"
>
<slot/>
</span>
</template>
<script>
export default {
name: 'optional-router-link',
props: {
params: Object,
disabled: {
type: Boolean,
default: false,
},
},
};
</script>
Optional, register globally:
Vue.component('optional-router-link', OptionalRouterLink);
Use it as follows:
<optional-router-link
:disabled="isDisabled"
:to="whatever"
>
My link
</optional-router-link>
The problem is that router-link renders as an html anchor tag, and anchor tags do not support the disabled attribute. However you can add tag="button" to router-link:
<router-link :to="myLink" tag="button" :disabled="isDisabled" >
Vue will then render your link as a button, which naturally supports the disabled attribute. Problem solved! The downside is that you have to provide additional styling to make it look like a link. However this is the best way to achieve this functionality and does not rely on any pointer-events hack.
I sometimes do stuff like this:
<component
:is="hasSubLinks ? 'button' : 'router-link'"
:to="hasSubLinks ? undefined : href"
:some-prop="computedValue"
#click="hasSubLinks ? handleClick() : navigate"
>
<!-- arbitrary markup -->
</component>
...
computed: {
computedValue() {
if (this.hasSubLinks) return 'something';
if (this.day === 'Friday') return 'tgif';
return 'its-fine';
},
},
But I basically always wrap router-link, so you can gain control over disabled state, or pre-examine any state or props before rendering the link, with something like this:
<template>
<router-link
v-slot="{ href, route, navigate, isActive, isExactActive }"
:to="to"
>
<a
:class="['nav-link-white', {
'nav-link-white-active': isActive,
'opacity-50': isDisabled,
}]"
:href="isDisabled ? undefined : href"
#click="handler => handleClick(handler, navigate)"
>
<slot></slot>
</a>
</router-link>
</template>
<script>
export default {
name: 'top-nav-link',
props: {
isDisabled: {
type: Boolean,
required: false,
default: () => false,
},
to: {
type: Object,
required: true,
},
},
data() {
return {};
},
computed: {},
methods: {
handleClick(handler, navigate) {
if (this.isDisabled) return undefined;
return navigate(handler);
},
},
};
</script>
In my app right now, I'm noticing that some combinations of #click="handler => handleClick(handler, navigate)" suffer significantly in performance.
For example this changes routes very slow:
#click="isDisabled ? undefined : handler => navigate(handler)"
But the pattern in my full example code above works and has no performance issue.
In general, ternary operator in #click can be very dicey, so if you get issues, don't give up right away, try many different ways to bifurcate on predicates or switch over <component :is="" based on state. navigate itself is an ornery one because it requires the implicit first parameter to work.
I haven't tried, but you should be able to use something like Function.prototype.call(), Function.prototype.apply(), or Function.prototype.bind().
For example, you might be able to do:
#click="handler => setupNavigationTarget(handler, navigate)"
...
setupNavigationTarget(handler, cb) {
if (this.isDisabled) return undefined;
return this.$root.$emit('some-event', cb.bind(this, handler));
},
...
// another component
mounted() {
this.$root.$on('some-event', (navigate) => {
if (['Saturday', 'Sunday'].includes(currentDayOfTheWeek)) {
// halt the navigation event
return undefined;
}
// otherwise continue (and notice how downstream logic
// no longer has to care about the bound handler object)
return navigate();
});
},
You could also use the following:
<router-link class="Card__link" :to="id ? { name: 'Property', params: { id: id }} : {}">
<h1 class="Card__title">{{ title }}</h1>
<p class="Card__description">{{ description }}</p>
</router-link>
If id is undefined the router won't redirect the page to the link.
I've tried different solutions but only one worked for me, maybe because I'm running if from Nuxt? Although theoretically nuxt-link should work exactly the same as router-link.
Anyway, here is the solution:
<template>
<router-link
v-slot="{ navigate }"
custom
:to="to"
>
<button
role="link"
#click="onNavigation(navigate, $event)"
>
<slot></slot>
</button>
</router-link>
</template>
<script>
export default {
name: 'componentName',
props: {
to: {
type: String,
required: true,
},
},
methods: {
onNavigation(navigate, event) {
if (this.to === '#other-action') {
// do something
} else {
navigate(event);
}
return false;
},
};
</script>