Identify if any data changing across all the Components - Vuejs - javascript

In my single page application, a screen consist of multiple components. I want to know is there any data has been modified/changed in the screen across all the component when we move to next screen.
For example, I am having Vue class "createProject.vue":
<template>
<Comments></Comments>
<ProjectSelection></ProjectSelection>
<MessageArea></MessageArea>
</template>
class Comments.vue,
<template>
<v-textarea
id="input--comments"
name="Comments"
></v-textarea>
</template>
class ProjectSelection.vue,
<template>
<div>
<v-text-field
id="input--email"
:label="Email"
></v-text-field>
</div>
</template>
class MessageArea.vue,
<template>
<v-textarea
id="input--message-area"
name="message"
></v-textarea>
</template>
When I move to the next screen, I want to know is there any data has been changed or not.
Kindly help me to identify the data changes across all the components.

In the root component:
<template>
<Comments #modified="dataUpdated = true"></Comments>
<ProjectSelection #modified="dataUpdated = true"></ProjectSelection>
<MessageArea #modified="dataUpdated = true"></MessageArea>
</template>
<script>
export default
{
data()
{
return {
dataModified: false,
nextRoute: null,
}
},
created()
{
window.addEventListener('beforeunload', this.pageLeave);
},
beforeDestroy()
{
window.removeEventListener('beforeunload', this.pageLeave);
},
beforeRouteLeave(to, from, next)
{
this.routeLeave(to, from, next);
},
methods:
{
routeLeave(to, from, next)
{
if(this.dataModified)
{
this.nextRoute = next;
// show dialog to the user that there is unsaved data - it might call this.ignoreUnsaved
}
},
ignoreUnsaved()
{
// hide the dialog
if (this.nextRoute) this.nextRoute();
},
pageLeave(e)
{
if(this.dataModified)
{
const confirmationMessage = 'There is unsaved data. Ignore it and continue?';
(e || window.event).returnValue = confirmationMsg;
return confirmationMessage;
}
},
}
}
</script>
In Comments.vue:
<template>
<v-textarea v-model.trim="newComment" />
</template
<script>
export default
{
data()
{
return {
trackThisForChanges:
{
newComment: '',
},
}
},
watch:
{
trackThisForChanges:
{
deep: true,
handler()
{
this.$emit('modified');
}
}
}
}

Related

How to pass initial form values to child component in Vue.js?

I'm using Vue.js. From my template I include the child component (componentB) which includes several input elements. I want to initialize those input elements from my parent template. I found a way to do this (see code below). However, I'm wondering if this is a correct way, as the articles I have read so far use different approaches (e.g. with $emit):
https://simonkollross.de/posts/vuejs-using-v-model-with-objects-for-custom-components
https://zaengle.com/blog/using-v-model-on-nested-vue-components
https://alligator.io/vuejs/add-v-model-support/
Can you confirm that my code below matches the Vue.js design concepts or are there flaws?
<template>
<div>
<div class="md-layout">
<div class="md-layout-item md-size-100">
<ComponentB ref="componentB" v-model="componentB"></ComponentB>
</div>
</div>
</div>
</template>
<script>
import { ComponentB } from "#/components";
export default {
components: {
ComponentB
},
data() {
return {
componentB: {
textInputField: "my-initial-value"
}
};
},
methods: {
validate() {
return this.$refs.componentB.validate().then(res => {
this.$emit("on-validated", res);
return res;
});
}
}
};
</script>
<style></style>
Form componentB
<template>
<div>
<md-field
:class="[
{ 'md-valid': !errors.has('textInputField') && touched.textInputField },
{ 'md-form-group': true },
{ 'md-error': errors.has('textInputField') }
]"
>
<md-icon>label_important</md-icon>
<label>My text input</label>
<md-input
v-model="textInputField"
data-vv-name="textInputField"
type="text"
name="textInputField"
required
v-validate="modelValidations.textInputField"
>
</md-input>
<slide-y-down-transition>
<md-icon class="error" v-show="errors.has('textInputField')"
>close</md-icon
>
</slide-y-down-transition>
<slide-y-down-transition>
<md-icon
class="success"
v-show="!errors.has('textInputField') && touched.textInputField"
>done</md-icon
>
</slide-y-down-transition>
</md-field>
</div>
</template>
<script>
import { SlideYDownTransition } from "vue2-transitions";
export default {
name: "componentB",
props: ['value'],
components: {
SlideYDownTransition
},
computed: {
textInputField: {
get() {return this.value.textInputField},
set(textInputField) { this.$emit('input', { ...this.value, ['textInputField']: textInputField })}
}
},
data() {
return {
touched: {
textInputField: false
},
modelValidations: {
textInputField: {
required: true,
min: 5
}
}
};
},
methods: {
getError(fieldName) {
return this.errors.first(fieldName);
},
validate() {
return this.$validator.validateAll().then(res => {
return res;
});
}
},
watch: {
textInputField() {
this.touched.runnerName = true;
}
}
};
</script>
<style></style>
The simplest way to pass data to child component is to use props, which are then available in the child component and can pass the values back up to the parent.
https://v2.vuejs.org/v2/guide/components-props.html
// PARENT COMPONENT
<ComponentB :textInputField="textInputField" ...></ComponentB>
// CHILD COMPONENT
// TEMPLATE SECTION
<md-input
v-model="textInputField"
value="textInputField"
...
>
// SCRIPT SECTION
export default {
props: {
textInputField: String
}
}

How to create infinite scroll in Vuetify Autocomplete component?

I have a page with Vuetify Autocomplete component, and REST API backend with '/vendors' method. This method takes limit, page and name parameters and returns JSON with id and name fields.
I made some code with lazy list load on user input event. But now I want to add the ability to load this list on user scroll event.
For example, by default there is a list of 100 vendors. User scrolled this list until the end, then "some event" is called and loads next 100 of vendors. Then user keeps scrolling and the action is repeated.
Is it possible to made this with Vuetify Autocomplete component, or should i use another library?
Example code of current component is shown below:
<template>
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
></v-autocomplete>
</template>
<script>
export default {
data () {
return {
page: 0,
limit: 100,
selectedVendorId: null,
vendors: [],
loading: true
}
},
created: function (){
this.getVendorsFromApi();
},
methods: {
getVendorsFromApi (event) {
return new Promise(() => {
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + this.limit
+ '&page=' + this.page
+ '&name=' + (event ? event.target.value : ''))
.then(response => {
this.vendors = response.data;
})
})
}
}
}
</script>
I was able to get auto-loading going with the Vuetify AutoComplete component. It's a bit of a hack, but basically the solution is to use the v-slot append item, the v-intersect directive to detect if that appended item is visible, and if it is, call your API to fetch more items and append it to your current list.
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
>
<template v-slot:append-item>
<div v-intersect="endIntersect" />
</template>
</v-autocomplete>
...
export default {
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting) {
let moreVendors = loadMoreFromApi()
this.vendors = [ ...this.vendors, ...moreVendors]
}
}
}
}
In my use case, I was using API Platform as a backend, using GraphQL pagination using a cursor.
<component
v-bind:is="canAdd ? 'v-combobox' : 'v-autocomplete'"
v-model="user"
:items="computedUsers"
:search-input.sync="search"
item-text="item.node.userProfile.username"
hide-details
rounded
solo
:filter="
(item, queryText, itemText) => {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
} "
:loading="loading"
item-value="username"
class="text-left pl-1"
color="blue-grey lighten-2"
:label="label"
>
<template v-slot:selection="{ item }">
<v-chip v-if="typeof item == 'object'">
<v-avatar left>
<v-img v-if="item.node.userProfile.image" :src="item.node.userProfile.image" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-avatar>
{{ item.node.userProfile.firstName }} {{ item.node.userProfile.lastName }}
</v-chip>
<v-chip v-else-if="typeof item == 'string'">
{{ item }}
</v-chip>
</template>
<template v-slot:item="{ item: { node } }">
<v-list-item-avatar >
<img v-if="node.userProfile.avatar" :src="node.userProfile.avatar" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-list-item-avatar>
<v-list-item-content class="text-left">
<v-list-item-title>
{{ $t('fullName', { firstName: node.userProfile.firstName, lastName: node.userProfile.lastName } )}}
</v-list-item-title>
<v-list-item-subtitle v-html="node.userProfile.username"></v-list-item-subtitle>
</v-list-item-content>
</template>
<template v-slot:append-item="">
<div v-intersect="endIntersect" >
</div>
</template>
</component>
import { VCombobox, VAutocomplete } from "vuetify/lib";
import debounce from "#/helpers/debounce"
import { SEARCH_USER_BY_USERNAME } from "#/graphql/UserQueries";
const RESULTS_TO_SHOW = 5
export default {
props: {
canAdd: {
type: Boolean,
default: false,
},
value: [Object, String],
label: String,
},
components: { VCombobox, VAutocomplete },
apollo: {
users: {
query: SEARCH_USER_BY_USERNAME,
variables() {
return {
username: this.search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
}
},
watchLoading(isLoading) {
this.loading = isLoading
},
skip() {
if (this.search) {
return !(this.search.length > 1)
}
return true
},
},
},
data() {
return {
user: this.value,
search: "",
cursor: null,
loading: false,
};
},
watch: {
user(newValue) {
let emit = newValue
if (newValue) {
emit = newValue.node
}
this.$emit("input", emit);
},
value(newValue) {
if (this.user && this.user.node != newValue) {
if (newValue == null) {
this.user = null
}
else {
this.user = { node: newValue };
}
}
},
search(newValue) {
this.debouncedSearch(newValue)
},
},
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting && this.users && this.users.pageInfo.hasNextPage) {
let cursor = this.users.pageInfo.endCursor
this.$apollo.queries.users.fetchMore({
variables: { cursor: cursor},
updateQuery: (previousResult, { fetchMoreResult }) => {
let edges = [
...previousResult.users.edges,
...fetchMoreResult.users.edges,
]
let pageInfo = fetchMoreResult.users.pageInfo;
return {
users: {
edges: edges,
pageInfo: pageInfo,
__typename: previousResult.users.__typename,
}
}
}
})
}
},
debouncedSearch: debounce(function (search) {
if (this.users) {
this.$apollo.queries.users.refetch({
username: search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
});
}
}, 500),
filter(item, queryText) {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
}
},
computed: {
computedUsers() {
if (this.users){
return this.users.edges
}
return []
},
skip() {
if (this.search) {
return this.search.length > 1
}
return false
}
}
};
</script>
Update June 12, 2021:
If you are using Vuetify 2.X, use Brettins' solution based on append-item slot and v-intersect directive.
Old answer:
Looks like it's not possible with default v-autocomplete component (at least in vuetify 1.5.16 or lower).
The component that provides the most similar functionality is VueInfiniteAutocomplete.
But keep in mind that in this case there may be problems with styles, validation, etc.
There is an example with this library.
<template>
<div>
<vue-infinite-autocomplete
:data-source="getAsyncOptions"
:fetch-size="limit"
v-on:select="handleOnSelect"
:value="autocompleteViewValue"
>
</vue-infinite-autocomplete>
</div>
</template>
<script>
export default {
data () {
return {
selectedVendorId : null,
limit: 100,
autocompleteViewValue: null
}
},
methods: {
getAsyncOptions(text, page, fetchSize) {
return new Promise((resolve, reject) => {
resolve(
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + fetchSize
+ '&page=' + page
+ '&name=' + text)
.then(response => {
//Response MUST contain 'id' and 'text' fields, and nothing else.
//If there are other fields, you should remove it here
//and create 'id' and 'text' fields in response JSON by yourself
return response.data;
})
)
});
},
handleOnSelect(selectedItem) {
this.autocompleteViewValue = selectedItem.text;
this.selectedVendorId = selectedItem.id;
}
}
}
</script>
P.S.: If you just want to use v-autocomplete component with server-side pagination, you could create a "Load more..." button using append-item slot, as suggested in this issue.

Vue.js: Show/Hide buttons on navbar based on Vuex Store state when the user is logged in or not

I'm creating a navbar that shows or hides buttons depending if the user is logged in or not.
For that, I'm saving the state on Vuex and localStorage.
I'm trying to build a dynamic menu, using a list of objects (i.e. rightMenu) that contains the information of the buttons (i.e. route, title and a flag that indicates if the button may show or not if the user is logged in).
Always that the user logs in the system, the this.$store.state.auth.isUserLoggedIn changes to true, however the template does not change, the button stays in the same initial state when the user was not logged in.
For example: the sign out button does not show when this.$store.state.auth.isUserLoggedIn updates.
But when I click 'ctrl+F5' and the page reloads, the buttons show correctly.
In this case, for example, the sign out button appears correctly when I reload the page manually.
I'm thinking in to force the page to reload again when the user logs in or logs out, however I believe that it is not a good option.
Could anyone help me?
I'm giving the code that I'm using below.
Thank you in advance.
Menu.vue > template
<div>
<v-toolbar color='grey darken-3' dark>
<v-toolbar-title>Site</v-toolbar-title>
...
<v-toolbar-items class='hidden-sm-and-down'>
<v-btn v-for='item in rightMenu' :key='item.title'
:to='item.to' v-if='item.showButton' flat>
{{ item.title }}
</v-btn>
</v-toolbar-items>
</v-toolbar>
<router-view/>
</div>
Menu.vue > script
export default {
data () {
return {
rightMenu: [
{ to: '/sign_in', title: 'sign in'
showButton: !this.$store.state.auth.isUserLoggedIn },
{ to: '/sign_up', title: 'sign up'
showButton: !this.$store.state.auth.isUserLoggedIn },
{ to: '/sign_out', title: 'sign out'
showButton: this.$store.state.auth.isUserLoggedIn }
]
}
},
...
}
store.js
const store = new Vuex.Store({
state: {
auth: {
token: '',
isUserLoggedIn: false
}
},
mutations: {
setAuthToken (state, token) { // I use it on the Login
state.auth.token = token
state.auth.isUserLoggedIn = !!token
localStorage.setItem('store', JSON.stringify(state))
},
cleanAuth (state) { // I use it on the Logout
state.auth = {
token: '',
isUserLoggedIn: false
}
localStorage.setItem('store', JSON.stringify(state))
}
}
...
})
EDIT 1:
When I use this.$store.state.auth.isUserLoggedIn explicitly on my code, it works well. So, the button appears and disappears correctly. I give below an example:
Menu.vue > template
<v-toolbar-items class='hidden-sm-and-down'>
<v-btn v-if='this.$store.state.auth.isUserLoggedIn' flat>
Test {{ this.$store.state.auth.isUserLoggedIn }}
</v-btn>
</v-toolbar-items>
Hence, I believe that the problem is in the binding of showButton with this.$store.state.auth.isUserLoggedIn.
Use computed property to make it reactive:
<template>
...
<v-btn v-for='item in rightMenu' :key='item.title'
:to='item.to' v-if='isUserLoggedIn(item.title)' flat>
{{ item.title }}
</v-btn>
...
</template>
<script>
...
computed: {
isUserLoggedIn() {
return (title) => { // you'll not have any caching benefits
if (title === 'sign out') {
return this.$store.state.auth.isUserLoggedIn;
}
return !this.$store.state.auth.isUserLoggedIn;
}
}
}
...
</script>
Through the answers of Chris Li, Andrei Gheorghiu and Sajib Khan I could solve my problem.
Andrei Gheorghiu have explained that I can't access computed properties in data() and Chris Li suggested that I use a computed variable instead. These answers plus Sajib Khan example I was able to think in a solution that I share below. I hope that it help others in the future.
In a nutshell, I've created a computed property that returns my array and always when this.$store.state.auth.isUserLoggedIn updates, the array changes together (consequently the menu as well).
I intent to create a mapGetter to my this.$store.state.auth.isUserLoggedIn. As soon as I do it, I update the answer.
Thank you guys so much.
<script>
export default {
data () {
return { ... }
},
computed: {
rightMenu () {
return [
{ title: 'sign_in', to: '/sign_in',
showButton: !this.$store.state.auth.isUserLoggedIn },
{ title: 'sign_up', to: '/sign_up',
showButton: !this.$store.state.auth.isUserLoggedIn },
{ title: 'sign_out', to: '/sign_out',
showButton: this.$store.state.auth.isUserLoggedIn }
]
}
}
}
</script>
EDIT 1: Solution using mapGetters
Menu.vue
<script>
import { mapGetters } from 'vuex'
export default {
data () {
return { ... }
},
computed: {
...mapGetters([
'isUserLoggedIn'
]),
rightMenu () {
return [
{ title: 'sign_in', to: '/sign_in',
showButton: !this.$store.state.auth.isUserLoggedIn },
{ title: 'sign_up', to: '/sign_up',
showButton: !this.$store.state.auth.isUserLoggedIn },
{ title: 'sign_out', to: '/sign_out',
showButton: this.$store.state.auth.isUserLoggedIn }
]
}
}
}
</script>
store.js
I've added the following getter:
...
getters: {
isUserLoggedIn (state) {
return state.auth.isUserLoggedIn
}
}
...

How do you target a button, which is in another component, with a method in App.vue?

I'm making a basic To Do app, where I have an input field and upon entering a task and pressing the "Enter" key the task appears in the list. Along with the task the TodoCard.vue component also generates a button, which I would like to use to delete the task.
I've added a #click="removeTodo" method to the button, but don't know where to place it, in the TodoCard component or in the App.vue file?
TodoCard component:
<template>
<div id="todo">
<h3>{{ todo.text }}</h3>
<button #click="removeTodo(todo)">Delete</button>
</div>
</template>
<script>
export default {
props: ['todo'],
methods: {
removeTodo: function (todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
}
}
}
</script>
App.vue:
<template>
<div id="app">
<input class="new-todo"
placeholder="Enter a task and press enter."
v-model="newTodo"
#keyup.enter="addTodo">
<TodoCard v-for="(todo, key) in todos" :todo="todo" :key="key" />
</div>
</template>
<script>
import TodoCard from './components/TodoCard'
export default {
data () {
return {
todos: [],
newTodo: ''
}
},
components: {
TodoCard
},
methods: {
addTodo: function () {
// Store the input value in a variable
let inputValue = this.newTodo && this.newTodo.trim()
// Check to see if inputed value was entered
if (!inputValue) {
return
}
// Add the new task to the todos array
this.todos.push(
{
text: inputValue,
done: false
}
)
// Set input field to empty
this.newTodo = ''
}
}
}
</script>
Also is the code for deleting a task even correct?
You can send an event to your parent notifying the parent that the delete button is clicked in your child component.
You can check more of this in Vue's documentation.
And here's how your components should look like:
TodoCard.vue:
<template>
<div id="todo">
<h3>{{ todo.text }}</h3>
<button #click="removeTodo">Delete</button>
</div>
</template>
<script>
export default {
props: ['todo'],
methods: {
removeTodo: function (todo) {
this.$emit('remove')
}
}
}
</script>
App.vue:
<template>
<div id="app">
<input class="new-todo"
placeholder="Enter a task and press enter."
v-model="newTodo"
#keyup.enter="addTodo">
<TodoCard v-for="(todo, key) in todos" :todo="todo" :key="key" #remove="removeTodo(key)" />
</div>
</template>
<script>
import TodoCard from './components/TodoCard'
export default {
data () {
return {
todos: [],
newTodo: ''
}
},
components: {
TodoCard
},
methods: {
addTodo: function () {
// Store the input value in a variable
let inputValue = this.newTodo && this.newTodo.trim()
// Check to see if inputed value was entered
if (!inputValue) {
return
}
// Add the new task to the todos array
this.todos.push(
{
text: inputValue,
done: false
}
)
// Set input field to empty
this.newTodo = ''
}
},
removeTodo: function(key) {
this.todos.splice(key, 1);
}
}
</script>

Cannot make vue.js element-ui's dialog work while it's inside a child component

Here is the parent component:
<template lang="pug">
.wrapper
el-button(type="primary", #click="dialogAddUser = true") New User
hr
// Dialog: Add User
add-edit-user(:dialog-visible.sync="dialogAddUser")
</template>
<script>
import * as data from '#/components/partials/data'
import AddUser from './partials/AddUser'
export default {
name: 'users',
components: { AddUser },
data () {
return {
users: data.users,
dialogAddUser: false
}
}
}
</script>
Here is the child component:
<template lang="pug">
el-dialog(width="75%", title="New User", :visible.sync="dialogVisible", top="5vh")
div 'el-dialog-body' - content goes here
</template>
<script>
export default {
name: 'add-user',
props: {
dialogVisible: Boolean
}
}
</script>
I am able to open the dialog but when close the dialog using top right button inside the dialog then I am getting this error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"dialogVisible"
Later I tried to play and did something like below, but now I cannot even open the dialog:
<template lang="pug">
el-dialog(width="75%", title="New User", :visible.sync="visibleSync", top="5vh")
div 'el-dialog-body' - content goes here
</template>
<script>
export default {
name: 'add-user',
props: {
dialogVisible: Boolean
},
watch: {
visibleSync (val) {
this.$emit('update:dialogVisible', val)
}
},
data () {
return {
visibleSync: this.dialogVisible
}
}
}
</script>
If visible.sync works, the component is emitting an update:visible event.
So, to not mutate in the child and, instead, propagate the event to the parent, instead of:
:visible.sync="dialogVisible"
Do
:visible="dialogVisible", v-on:update:visible="visibleSync = $event"
Full code:
<template lang="pug">
el-dialog(width="75%", title="New User", :visible="dialogVisible", v-on:update:visible="visibleSync = $event", top="5vh")
div 'el-dialog-body' - content goes here
</template>
<script>
export default {
name: 'add-user',
props: {
dialogVisible: Boolean
},
watch: {
visibleSync (val) {
this.$emit('update:dialogVisible', val)
}
},
data () {
return {
visibleSync: this.dialogVisible
}
}
}
</script>
As another alternative, you could emit directly from the v-on listener and do without the visibleSync local property:
<template lang="pug">
el-dialog(width="75%", title="New User", :visible="dialogVisible", v-on:update:visible="$emit('update:dialogVisible', $event)", top="5vh")
div 'el-dialog-body' - content goes here
</template>
<script>
export default {
name: 'add-user',
props: {
dialogVisible: Boolean
}
}
</script>
I think a nice way to handle this is to:
Use a prop to pass the visible state from the parent to the child.
Forward el-dialog's close event from the child to the parent.
In the parent, handle the close event to set the prop to false.
Child:
<el-dialog :visible="visible" #close="$emit('close')">
export default {
props: {
visible: Boolean
},
...
Parent (assuming you store the open state in state.modalOpen):
<el-button #click="state.modalOpen = true">Open Modal</el-button>
<child-component :visible="state.modalOpen" #close="state.modalOpen = false" />

Categories

Resources