In our frontend, we want to disable certain elements (i.e. buttons) depending on whether a user has "permissions" to trigger an action or not. To keep our code clean, we would like to do something like this:
<button reqPermission="edit">
This should call a JS method connecting to our permission service and Vue should only render the element / enable it if the request is truthy.
I don't have much clue of VueJS - what I would like to avoid though, is using something like v-if="...", since this would clutter our code. Any hints on how to implement such a "custom attribute" that influences the rendering of a component would be highly appreciated. What I found so far is https://forum.vuejs.org/t/how-to-conditionally-render-a-component/69687/6
If you want to disable it based on a boolean you can do this inside the Vue template syntax:
<button :disabled="!userRights.edit">
If you want to prevent rendering instead of disabling the button, use v-if instead of :disabled.
If the boolean has to be loaded from a backend, I'd recommend using a vuex store action to fetch the user rights and conditional rendering based on the store state:
<template>
<button :disabled="!isLoading && !userRights.edit">
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'buttonComponent',
computed: {
...mapState('userModule', ['isLoading', 'userRights']),
},
mounted() {
this.$store.dispatch('userModule/getRights')
},
}
</script>
export default {
namespaced: true,
state: {
isLoading: false,
userRights: {
edit: false,
view: false,
},
},
mutations: {
updateIsLoading(state, isLoading) {
state.isLoading = isLoading
},
updateUserRights(state, userRights) {
state.userRights = userRights
},
},
actions: {
async getRights() {
this.commit('userModule/updateIsLoading', true)
const user = await getCurrentUserID()
const rights = await getRightsByUser(user) // backend call
this.commit('userModule/updateUserRights', rights)
this.commit('userModule/updateIsLoading', false)
},
},
}
Related
I'm sending from the parent component a prop: user. Now in the child component I want to make a copy of it without it changing the prop's value.
I tried doing it like this:
export default defineComponent({
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const user = ref(props.apiUser);
return { user };
}
});
But then if I change a value of the user object it also changes the apiUser prop. I thought maybe using Object.assign would work but then the ref isn't reactive anymore.
In Vue 2.0 I would do it like this:
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
return {
user: {}
}
},
mounted() {
this.user = this.apiUser;
// Now I can use this.user without changing this.apiUser's value.
}
};
Credits to #butttons for the comment that lead to the answer.
const user = reactive({ ...props.apiUser });
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const userCopy = toRef(props, 'apiUser')
}
With the composition API we have the toRef API that allows you to create a copy from any source reactive object. Since the props object is a reactive, you use toRef() and it won't mutate your prop.
This is what you looking for: https://vuejs.org/guide/components/props.html#one-way-data-flow
Create data where you add the prop to
export default {
props: ['apiUser'],
data() {
return {
// user only uses this.apiUser as the initial value;
// it is disconnected from future prop updates.
user: this.apiUser
}
}
}
Or if you use api composition:
import {ref} from "vue";
const props = defineProps(['apiUser']);
const user = ref(props.apiUser);
You also may want to consider using computed methods (see also linked doc section from above) or v-model.
Please note that the marked solution https://stackoverflow.com/a/67820271/2311074 is not working. If you try to update user you will see a readonly error on the console. If you don't need to modify user, you may just use the prop in the first place.
As discussed in comment section, a Vue 2 method that I'm personally fond of in these cases is the following, it will basically make a roundtrip when updating a model.
Parent (apiUser) ->
Child (clone apiUser to user, make changes, emit) ->
Parent (Set changes reactively) ->
Child (Automatically receives changes, and creates new clone)
Parent
<template>
<div class="parent-root"
<child :apiUser="apiUser" #setUserData="setUserData" />
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of child component etc.)
export default {
data() {
apiUser: {
id: 'e134',
age: 27
}
},
methods: {
setUserData(payload) {
this.$set(this.apiUser, 'age', payload);
}
}
}
Child
<template>
<div class="child-root"
{{ apiUser }}
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of components etc.)
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
user: null
},
watch: {
apiUser: {
deep: true,
handler() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
}
}
},
mounted() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
},
methods: {
// Whatever function catching the changes you want to do
setUserData(payload) {
this.$emit('setUserData', this.user);
}
}
}
Apologies for any miss types
I'm so confused how I should submit and handle edit forms in vuejs.
How I'm doing it now is I have a component called TreeForm.vue:
<template>
<div>
<v-text-field v-model="cloned_tree.root" />
<v-text-field v-model="cloned_tree.root" />
<v-file-input type="number" v-model="cloned_tree.fruits" />
<v-btn #click="$emit('save', {idx: tree_idx, tree: cloned_tree})">Save</v-btn>
</div>
</template>
<script>
export default {
props: {tree_idx: Number},
data() {
return {
cloned_tree: JSON.parse(JSON.stringify(this.$store.state.trees[this.tree_idx])),
};
},
};
</script>
And in the parent component I do:
<template>
<div>
...
<TreeForm tree_idx="0" #save="submitTreeForm" />
...
</div>
</template>
<script>
import {mapActions} from 'vuex';
export default {
methods: {
...mapActions(['submitTreeForm']),
},
};
</script>
And in my vuex I do:
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
const api = axios.create({
baseURL: 'https://api.mydomain.com/api',
timeout: 10000,
withCredentials: true,
});
Vue.use(Vuex);
export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
state: {
trees: [
{
root: 'hello',
imageFile: require('some/picture'),
fruits: 5,
},
],
},
mutations: {
updateTree(state, payload) {
state.trees[payload.idx] = payload.tree;
},
},
actions: {
submitVideoForm({commit}, payload) {
api
.post('/trees/update/', payload)
.then(response => {
if (response.data.success == 1) {
commit('updateTree', payload);
} else {
console.log(response.data.success);
}
})
.catch(function(error) {
console.log(error);
});
},
},
});
But I feel like This is not the correct way to do it specially because I'm not using <v-form> or <form> tags. I also haven't incorporated validation yet, I'm thinking of using vuelidate. So please give me the best practice for submitting and handling edit form while validation is done by vuelidate.
Basically, the form tag is not mandatory. Unless using some kind of CSS framework, the UI will look the same with or without the form tag. But, it still has some pros:
Ben Nadel put it best in his blog post:
... it seems that using a FORM tag does have some benefits, depending
on your particular situation:
You need it if you want to execute a traditional (ie. non-AJAX) form
post.
You need it you want to capture the "submit" event
programmatically.
You need it in order for mobile Safari to show the
"Go" button on the keyboard.
You need it if you want to
programmatically "reset" a form (ie. call reset()).
It makes generic
form serialization easier (since it groups input fields).
You need it if you want to post files without the modern file API.
You need it if you have to segregate fields with the same name.
Vue.js gets you covered in almost all these situations. But if it doesn't - use form.
I have a Vue 2 application that uses an array of objects to back a search/multiselect widget provided by vue-multiselect.
I have looked at the Vue 1 -> 2 migration guide on debouncing calls, but the example they give did not propagate the arguments from the DOM elements to the business logic.
Right now the select fires change events with every keystroke, but I would like to throttle this (EG with lodash#throttle) so I'm not hitting my API every few milliseconds while they're typing.
import {mapGetters} from 'vuex';
import { throttle } from 'lodash';
import Multiselect from 'vue-multiselect'
export default {
components: {
Multiselect
},
data() {
return {
selectedWork: {},
works: [],
isLoading: false
}
},
computed: {
...mapGetters(['worksList']),
},
methods: {
getWorksAsync: throttle((term) => {
// the plan is to replace this with an API call
this.works = this.worksList.filter(work => titleMatches(work, term));
}, 200)
}
}
Problem: when the user types in the select box, I get the error:
TypeError: Cannot read property 'filter' of undefined
which is happening because this.worksList is undefined inside the throttle function.
Curiously, when I use the dev tools debugger, this.worksList has the value I need to dereference, with this referring to the Vue component.
Currently I am not calling the API from within the component, but the problem remains the same:
How can I throttle this call, and have the proper this context to update my this.works list? EDIT: this is explained in Vue Watch doesnt Get triggered when using axios
I also need to capture the user's query string from the multiselect widget to pass to the API call.
What is the proper pattern in Vue 2?
I ran into the same issue when using lodash.debounce. I'm a huge fan of arrow syntax, but I discovered that it was causing _.throttle() and _.debounce(), etc. to fail.
Obviously my code differs from yours, but I have done the following and it works:
export default {
...,
methods: {
onClick: _.debounce(function() {
this.$emit('activate', this.item)
}, 500)
}
}
Even though I'm not using arrow syntax here, this still references the component inside the debounced function.
In your code, it'd look like this:
export default {
...,
methods: {
getWorksAsync: throttle(function(term) {
// the plan is to replace this with an API call
this.works = this.worksList.filter(work => titleMatches(work, term));
}, 200)
}
}
Hope that helps!
I was unable to find an answer on SO (or anywhere) for this, but I eventually cobbled it together through trial and error, and from related materials here and in the docs.
Things that work that I didn't do, and why
It is possible to get get the value directly using a JavaScript DOM query, and it is also possible to dig in to the multiselect component's structure and get the value. The first solution circumvents the framework, the second depends on undocumented attributes of the multiselect component. I am avoiding both of those solutions as non-idiomatic and brittle.
My current solution
Updated an attribute on the component whenever there was a change event in the search box. This allowed me to capture the user's query string.
Called my throttled async function from inside the event listener.
Passed a regular function instead of an arrow function to throttle, which gave the correct this (the Vue component.)
If anyone has a suggestion for a better way to do this in Vue 2, I'm all ears.
Here's what my solution looked like in the end:
<template>
<div>
<label
class="typo__label"
for="ajax">Async select</label>
<multiselect
id="ajax"
v-model="selectedWork"
label="title"
track-by="id"
placeholder="Type to search"
:options="works"
:searchable="true"
:loading="isLoading"
:internal-search="false"
:multiple="false"
:clear-on-select="true"
:close-on-select="true"
:options-limit="300"
:limit="3"
:limit-text="limitText"
:max-height="600"
:show-no-results="false"
open-direction="bottom"
#select="redirect"
#search-change="updateSearchTerm">
<span slot="noResult">Oops! No elements found. Consider changing the search query.</span>
</multiselect>
</div>
</template>
<script>
import {mapGetters} from 'vuex';
import { throttle } from 'lodash';
import Multiselect from 'vue-multiselect'
export default {
components: {
Multiselect
},
data() {
return {
searchTerm: '',
selectedWork: {},
works: [],
isLoading: false
}
},
computed: {
...mapGetters(['worksList']),
},
methods: {
limitText(count) {
return `and ${count} other works`;
},
redirect(work) {
// redirect to selected page
},
updateSearchTerm(term){
this.searchTerm = term;
this.isLoading = true;
this.getWorksAsync();
},
getWorksAsync: throttle(function() {
const term = this.searchTerm.toLowerCase();
callMyAPI(term)
.then(results => {
this.works = results;
this.isLoading = false;
})
}, 200)
}
}
</script>
I am new to Vuex store which is served by vue.js. and i want to use it in following scenerio.
1.Does STATE means any data which is either static or dynamic which is served by server. or say data stored in json?
TEMPLATE.
<!-- title start -->
<div class="_Handler-0-1">
<x-x :path="store.glyph.path['0']":classs="store.static.class['0']"/>
</div>
<!-- title end -->
OBJECT
store: {
glyph: {
path: {
0: '<svg>.....</svg'>
}
},
static: {
class: {
0: 'icon-phone'
}
}
}
It's worth reading the documentation on vuex.
https://vuex.vuejs.org/en/intro.html
It's all about state management. You can retrieve data from the server and store it in there if you want to or you can store user entered data. It's very flexible, but the way it is designed is to ensure that it is always managed correctly
Vuex' has a functional life-cycle :
Dispatchers
Actions
Mutations
Getters
Dispatchers .dispatch Actions
Actions commit Mutations
Mutations mutate (change) the state
Getters return parts of the state.
I don't know the full setup of how you've done your store but to retrieve the two parts of your state i would write a getter that returns both parts.
const store = new Vuex.Store({
state: {
glyph: {
path: '<svg>.....</svg>'
},
static: {
class: 'icon-phone'
}
},
getters: {
glyph: state => state.glyph,
static: state => state.static
}
})
<template>
<div class="_Handler-0-1">
<x-x :path="glyph.path":class="static.path"/>
</div>
</template>
<script>
import { ...mapGetters } from 'vuex'
export default {
name: 'foo',
computed: {
...mapGetters(['glyph', 'static'])
}
}
</script>
Also worth mentioning that static is a reserved word.
I'm using Vue v1.0.28 and vue-resource to call my API and get the resource data. So I have a parent component, called Role, which has a child InputOptions. It has a foreach that iterates over the roles.
The big picture of all this is a list of items that can be selected, so the API can return items that are selected beforehand because the user saved/selected them time ago. The point is I can't fill selectedOptions of InputOptions. How could I get that information from parent component? Is that the way to do it, right?
I pasted here a chunk of my code, to try to show better picture of my problem:
role.vue
<template>
<div class="option-blocks">
<input-options
:options="roles"
:selected-options="selected"
:label-key-name.once="'name'"
:on-update="onUpdate"
v-ref:input-options
></input-options>
</div>
</template>
<script type="text/babel">
import InputOptions from 'components/input-options/default'
import Titles from 'steps/titles'
export default {
title: Titles.role,
components: { InputOptions },
methods: {
onUpdate(newSelectedOptions, oldSelectedOptions) {
this.selected = newSelectedOptions
}
},
data() {
return {
roles: [],
selected: [],
}
},
ready() {
this.$http.get('/ajax/roles').then((response) => {
this.roles = response.body
this.selected = this.roles.filter(role => role.checked)
})
}
}
</script>
InputOptions
<template>
<ul class="option-blocks centered">
<li class="option-block" :class="{ active: isSelected(option) }" v-for="option in options" #click="toggleSelect(option)">
<label>{{ option[labelKeyName] }}</label>
</li>
</ul>
</template>
<script type="text/babel">
import Props from 'components/input-options/mixins/props'
export default {
mixins: [ Props ],
computed: {
isSingleSelection() {
return 1 === this.max
}
},
methods: {
toggleSelect(option) {
//...
},
isSelected(option) {
return this.selectedOptions.includes(option)
}
},
data() {
return {}
},
ready() {
// I can't figure out how to do it
// I guess it's here where I need to get that information,
// resolved in a promise of the parent component
this.$watch('selectedOptions', this.onUpdate)
}
}
</script>
Props
export default {
props: {
options: {
required: true
},
labelKeyName: {
required: true
},
max: {},
min: {},
onUpdate: {
required: true
},
noneOptionLabel: {},
selectedOptions: {
type: Array
default: () => []
}
}
}
EDIT
I'm now getting this warning in the console:
[Vue warn]: Data field "selectedOptions" is already defined as a prop. To provide default value for a prop, use the "default" prop option; if you want to pass prop values to an instantiation call, use the "propsData" option. (found in component: <default-input-options>)
Are you using Vue.js version 2.0.3? If so, there is no ready function as specified in http://vuejs.org/api. You can do it in created hook of the component as follows:
// InputOptions component
// ...
data: function() {
return {
selectedOptions: []
}
},
created: function() {
this.$watch('selectedOptions', this.onUpdate)
}
In your InputOptions component, you have the following code:
this.$watch('selectedOptions', this.onUpdate)
But I am unable to see a onUpdate function defined in methods. Instead, it is defined in the parent component role. Can you insert a console.log("selectedOptions updated") to check if it is getting called as per your expectation? I think Vue.js expects methods to be present in the same component.
Alternatively in the above case, I think you are allowed to do this.$parent.onUpdate inside this.$watch(...) - something I have not tried but might work for you.
EDIT: some more thoughts
You may have few more issues - you are trying to observe an array - selectedOptions which is a risky strategy. Arrays don't change - they are like containers for list of objects. But the individual objects inside will change. Therefore your $watch might not trigger for selectedOptions.
Based on my experience with Vue.js till now, I have observed that array changes are registered when you add or delete an item, but not when you change a single object - something you need to verify on your own.
To work around this behaviour, you may have separate component (input-one-option) for each of your input options, in which it is easier to observe changes.
Finally, I found the bug. I wasn't binding the prop as kebab-case