Step 5: Edit & Delete Accounts
This is part 5 of an 18 part series titled Vue.js Application Tutorial - Creating a Simple Budgeting App with Vue. It is highly recommended that you read the series in order, as each section builds on the last.
- Step 0: Intro
- Step 1: Planning Your Application
- Step 2: Data Architecture
- Step 3: Setup & Project Structure
- Step 4: Create & View Accounts
- Step 5: Edit & Delete Accounts
- Step 6: Adding LocalStorage to our Vue.js Application
- Step 7: Interlude & Refactor
- Step 8: Budgeting
- Step 9: Racing Through Budgets
- Step 10: Styling & Navigation
- Step 11: Finishing Budgets with Vue.js Dynamic Components
- Step 12: Planning for Transactions
- Step 13 - All Aboard the Transaction Train
- Step 14 - User Testing
The next entry is expected to be published on 03 August 2017. Sign up using the form at the bottom of this post to receive updates when posts are published.
Now that we can add and view all the existing accounts, it might be tempting to switch over to transactions and let the user create and view those right away. That would certainly feed my need for instant gratification. There is more work to be done with accounts, and we don't want to abandon them until we get to a stable place. Remember that we are working in a modular approach, which requires that we finish one section before moving to the next.
The next thing the user needs to do is edit their accounts. There is not actually much here to edit - just changing the name or category. Should we let them change the account balance after creation? Eventually the user will be updating the balance via transactions - reducing the balance when they make payments, and increasing the balance when they receive their paycheck. Once the account is created with a balance we should not let them edit it directly. (This is a feature of the software, and not a programming principle.)
Other than the lack of balance, our edit form looks identical to our create form. Our actions are slightly different though - we don't want to addAccount
so we will create a new updateAccount
. As you think through the code you might question our variable naming in CreateEditAccount.vue
. Right now the account we are creating is named newAccount
. We would like to simplify things and use the same variable when editing. We could leave it with that name and it would not change the functionality once we add edit capabilities. It might get confusing when we look back on this code later though.
This is a good time to make that change. Use your IDE's refactor tool or do a search/replace in the file and rename it to selectedAccount
.
We are working inside-out, so we start by adding mutators/actions to edit and delete an account.
export default {
ADD_ACCOUNT (state, payload) {
let id = guid();
state.accounts[id] = Object.assign({ id: id }, payload.account);
},
UPDATE_ACCOUNT (state, payload) {
state.accounts[payload.account.id] = payload.account;
},
DELETE_ACCOUNT (state, payload) {
Vue.delete(state.accounts, payload.account.id);
}
};
export const addAccount = ({ commit }, data) => {
commit('ADD_ACCOUNT', { account: data });
};
export const updateAccount = ({ commit }, data) => {
commit('UPDATE_ACCOUNT', { account: data });
};
export const deleteAccount = ({ commit }, data) => {
commit('DELETE_ACCOUNT', { account: data });
};
We will add edit and delete links from the list of accounts. The delete link will confirm their deletion, then delete without navigating away. The edit link will point to an edit route that will use the CreateEditAccount
component. We need to install the routing for that.
<template>
<div id="accounts-list-view">
I'm a list of accounts!
<router-link :to="{ name: 'createAccount' }">Add an account</router-link>
<ul>
<li v-for="account, key in accounts">
{{ account.name }}
<span class="tag is-small is-info">{{ categories[account.category] }}</span>
${{ account.balance }}
<a @click="confirmDeleteAccount(account)">Delete</a>
<router-link :to="{ name: 'editAccount', params: { accountId: account.id } }">Edit</router-link>
</li>
</ul>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import { CATEGORIES } from '../../../consts';
export default {
name: 'accounts-list-view',
data () {
return {
categories: CATEGORIES
};
},
methods: {
// this imports our vuex actions and maps them to methods on this component
...mapActions([
'deleteAccount'
]),
confirmDeleteAccount (account) {
// note that these are backticks and not quotation marks
if (confirm(`Are you sure you want to delete ${account.name}?`)) {
this.deleteAccount(account);
}
}
},
computed: {
...mapState({
'accounts': state => state.accounts.accounts
})
}
};
</script>
<style scoped lang='scss'>
#accounts-list-view {
}
</style>
import * as components from './components';
export default [
{
path: '/',
component: components.AccountsListView,
name: 'accountsListView'
},
{
path: '/accounts/create',
component: components.CreateEditAccount,
name: 'createAccount' // note that we changed this since we are using separate routes for create and edit
},
{
path: '/accounts/:accountId/edit', // the URL accepts an accountId parameter
component: components.CreateEditAccount,
name: 'editAccount'
}
];
Now the user can delete an account from the list - they are asked to confirm before the deletion occurs. And they can navigate to the edit page. That page doesn't load an account - it shows the blank form like the create page. It now is receiving the ID of the account we want to edit as a URL parameter. We can take that and look up an account.
First we need to load the actual account based on its ID. We already have a way to load in the entire list of accounts. It's tempting to do that, and then perform the lookup in the CreateEditComponent
. That solution brings up two problems. 1) One of the principles of Vuex - and a good overall programming practice - is to only give components access to the data they actually need access to. CreateEditComponent
doesn't need the entire list of accounts, only the one being edited. 2) There are probably going to be other times in our application where we need to pull a single account based off its ID. Wouldn't it be better to move that function to a central place?
I'm sold! Let's use Vuex to pull an account based off its ID, then pass that to our component. Where should this go in the store? It doesn't mutate anything or place an action, it simply gets a value from a list. Vuex provides us a getters
feature that would be perfect for this. We'll add getters.js
in the accounts/vuex
directory, and load it with our store. Once that is in place then we will tie in our UpdateAccount
action to CreateEditComponent
, looking for the accountId
parameter when the component is loaded to determine if we are editing or creating.
Can you think of another way we could perform this Edit task without using the URL to pass around IDs? We could add an
activeAccount
object to the store, load the account we want to edit into it, and then pull that object directly intoCreateEditComponent
. While this solution is shorter - avoiding having to look up an account by its ID - it adds data to our store that doesn't truly need to be shared globally. It also breaks our application if the user refreshes or leaves and returns to the edit page, since our store is wiped on page reload and there would be nothing in the URL telling the app what account to edit.
- src/app/accounts/vuex/getters.js
- src/app/accounts/vuex/index.js
- src/app/accounts/components/CreateEditAccount.vue
export default {
getAccountById: (state, getters) => (accountId) => {
return state.accounts && accountId in state.accounts ? state.accounts[accountId] : false;
}
};
import * as actions from './actions';
import getters from './getters';
import mutations from './mutations';
const state = {
accounts: {}
};
export default {
state,
actions,
mutations,
getters
};
<template>
<div id="accounts-create-edit-view">
You can create and edit accounts with me, yippee!
<router-link :to="{ name: 'accountsListView' }">View all accounts</router-link>
<form class="form" @submit.prevent="processSave">
<label for="name" class="label">Name</label>
<p class="control">
<input type="text" class="input" name="name" v-model="selectedAccount.name">
</p>
<label for="category" class="label">Category</label>
<p class="control">
<span class="select">
<select name="category" v-model="selectedAccount.category">
<option v-for="value, key in categories" :value="key">{{ value }}</option>
</select>
</span>
</p>
<label for="balance" class="label">Balance</label>
<p class="control">
<input type="text" class="input" name="balance" v-model="selectedAccount.balance">
</p>
<div class="control is-grouped">
<p class="control">
<button class="button is-primary">Submit</button>
</p>
<p class="control">
<router-link :to="{ name: 'accountsListView' }"><button class="button is-link">Cancel</button></router-link>
</p>
</div>
</form>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { CATEGORIES } from '../../../consts';
export default {
name: 'accounts-create-edit-view',
data: () => {
return {
categories: CATEGORIES,
selectedAccount: {},
editing: false
};
},
mounted () {
if ('accountId' in this.$route.params) {
let selectedAccount = this.getAccountById(this.$route.params.accountId);
if (selectedAccount) {
this.editing = true;
this.selectedAccount = selectedAccount;
}
// TODO: the object does not exist, how do we handle this scenario?
}
},
methods: {
...mapActions([
'addAccount',
'updateAccount'
]),
resetAndGo () {
this.selectedAccount = {};
this.$router.push({ name: 'accountsListView' });
},
saveNewAccount () {
this.addAccount(this.selectedAccount).then(() => {
this.resetAndGo();
});
},
saveAccount () {
this.updateAccount(this.selectedAccount).then(() => {
this.resetAndGo();
});
},
processSave () {
this.editing ? this.saveAccount() : this.saveNewAccount();
}
},
computed: {
...mapGetters([
'getAccountById'
])
}
};
</script>
<style scoped lang='scss'>
#accounts-create-edit-view {
}
</style>
It's worth noting that Vue-Router now gives us the option to pass URL data to the component as props. This feature was released after I initially wrote this section of the application, but it would help to further decouple our application. We may do a refactor later to include it.
This works great! Except for one problem. Take a minute to look for it. Ignore the gif below that demonstrates it!
Try this: start to edit an account, change the name, then cancel out to the accounts list without saving. Your edits are there?
It turns out we are editing the account object from the store directly - something we never want to do! Let's make a copy of the object that we can edit. We could clone it directly, but we actually only need the user to edit two fields. Temember that we don't want to let the user edit the balance of the account? That means we only need the name and type of the account to edit.
// src/app/accounts/components/CreateEditAccount.vue
<p class="control">
<input type="text" class="input" name="balance" v-model="selectedAccount.balance" v-if="!editing">
<span v-else>To update your balance, add a balance adjusting transaction.</span>
</p>
...
if (selectedAccount) {
this.editing = true;
this.selectedAccount = {
name: selectedAccount.name,
category: selectedAccount.category,
id: selectedAccount.id
};
}
Continue to step 6, Adding LocalStorage to our Vue.js Application