25
loading...
This website collects cookies to deliver better user experience
<template>
<div>
<h1>{{ title }}</h1> // 'Hello World'
<button @click="$emit('pass', 'foobar')">Pass to parent</button>
</div>
</template>
<script>
export default {
props: ['title']
}
</script>
<tempalte>
<div>
<h1>{{ childData }}</h1> // 'foobar' when btn clicked
<Child title="Hello world" @pass="childData = event"/>
</div>
</template>
<script>
export default {
data() {
return {
childData: ''
}
}
}
</script>
Vuex serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.
src/store/index.ts
file. I am using typescript with composition api, so an additional setup is required to utilize the autocompletion and intellisense.useStore
is used to return the typed store. For useStore
to correctly return the typed store, additional configuration is required:InjectionKey
.InjectionKey
when installing a store to the Vue app.InjectionKey
to the useStore method.import { InjectionKey } from 'vue'
import { createStore, useStore as baseUseStore, Store } from 'vuex'
export interface State {
count: number
}
export const key: InjectionKey<Store<State>> = Symbol()
export const store = createStore<State>({
state: {
count: 0
}
})
// define your own `useStore` composition function
export function useStore () {
return baseUseStore(key)
}
setup()
method.<script>
import { useStore } from "@/store/index";
import { defineComponent } from "vue";
export default defineComponent({
setup () {
const store = useStore()
store.state.count // typed as number
}
})
</script>
import router from "./router";
import { store, key } from "./store";
const app = createApp(App);
app.use(store, key).use(router).mount("#app");
types
.export default interface Goal {
id?: string;
title: string;
dueDate: string;
}
import { InjectionKey } from "vue";
import { createStore, useStore as baseUseStore, Store } from "vuex";
import Goal from "@/types/Goal.interface";
export interface State {
goalList: Goal[];
}
export const key: InjectionKey<Store<State>> = Symbol();
export const store = createStore<State>({
state: {
goalList: [
{
id: "e356583f-40fd-4827-96dd-21f556691921",
title: "Hello World",
dueDate: "31/12/2021",
},
],
},
});
export function useStore(): Store<State> {
return baseUseStore(key);
}
goalList
state with sample data, which can be pulled in the component.setup()
is very simple with useStore
. I have removed a dummy variable to toggle a visibility of the AppGoalListItem.vue
component. <script lang="ts">
import AppGoalForm from "@/components/AppGoalForm.vue";
import AppGoalListItem from "@/components/AppGoalListItem.vue";
import { defineComponent, ref } from "vue";
export default defineComponent({
components: {
AppGoalListItem,
AppGoalForm,
},
setup() {
const store = useStore();
const { goalList } = store.state;
return {
goalList,
};
},
});
</script>
AppGoalListItem.vue
must be made. <template>
<li class="p-10 border border-gray-200 rounded-md text-left">
<div class="flex items-center justify-between mb-5">
<p class="text-gray-400 text-sm">{{ formatDate(goal.dueDate, "DD/MM/YYYY") }}</p>
<div class="space-x-2 text-gray-400 -mt-5">
<button @click.stop="$emit('update')">
<PencilIcon class="h-5 w-5 hover:text-gray-700" />
</button>
<button @click.stop="$emit('delete')">
<TrashIcon class="h-5 w-5 hover:text-gray-700" />
</button>
</div>
</div>
<div class="flex items-center">
<p class="text-lg font-light">
{{ goal.title }}
</p>
</div>
</li>
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { PencilIcon, TrashIcon } from "@heroicons/vue/outline";
import Goal from "@/types/Goal.interface";
export default defineComponent({
components: {
PencilIcon,
TrashIcon,
},
props: {
goal: {
type: Object as PropType<Goal>,
required: true,
},
},`
emits: ["update", "delete"],
setup() {
function formatDate(date: string, format: string): string {
return moment(date, format, true).format(format);
}
return {
formatDate,
}
}
});
</script>
AppGoalListItem.vue
component, I have also added a few simple "emit" methods as I would like view
components to do the communication with Vuex. This way a component can be relatively independent.AppGoalListItem.vue
component updated, it is just a matter of passing a data from Home.vue
with props.<template>
<BaseLayout>
<template #header>
<h1 class="h-title">My Goals</h1>
</template>
<p class="text-sm italic text-gray-500">You can have up to 2 goals</p>
<ul class="mt-5 space-y-8">
<RouterLink v-slot="{ navigate }" to="/weekly-plan" custom>
<AppGoalListItem
v-for="goal in goalList"
:key="goal.id"
:goal="goal"
class="hover:bg-gray-50 cursor-pointer"
@click="navigate"
/>
</RouterLink>
</ul>
</BaseLayout>
</template>
goalList
state. A Vuex state should be changed via mutations
. They are rules ensuring that the state can only be mutated predictably.export const store = createStore<State>({
state: {
goalList: [
{
id: "e356583f-40fd-4827-96dd-21f556691921",
title: "Hello World",
dueDate: "31/12/2021",
},
],
},
mutations: {
addGoal(state: State, goal: Goal): void {
goal.id = uuidv4();
state.goalList.push(goal);
},
},
});
AppGoalForm.vue
component. Similiar to AppGoalListItem.vue
, a component will not interact with a state, only a view, so additional events must be emiited.<template>
<div>
<button
v-if="!isVisibleGoalForm"
class="py-10 border border-gray-200 rounded-md hover:bg-gray-50 w-full"
@click="showGoalInput()"
>
+ Add Goal
</button>
<form v-else class="space-y-2" @submit.prevent="submit()">
<ul>
<li
v-for="error in errors"
:key="error"
class="list-disc list-inside text-red-500 text-sm"
>
{{ error }}
</li>
</ul>
<textarea
ref="goalTextArea"
class="px-5 py-2 w-full border border-gray-200 rounded-md resize-none"
rows="3"
placeholder="Write down your S.M.A.R.T goal here ..."
:value="title"
@input="$emit('update:title', $event.target.value)"
></textarea>
<input
class="px-5 py-2 w-full border border-gray-200 rounded-md resize-none"
type="text"
placeholder="Achieve your goal by dd/mm/yyyy"
:value="dueDate"
@input="$emit('update:dueDate', $event.target.value)"
/>
<div class="flex justify-end space-x-3">
<button
type="button"
class="action-btn border border-gray-200 hover:bg-gray-100"
@click.stop="hideGoalInput()"
>
Cancel
</button>
<button
type="submit"
class="action-btn bg-green-400 text-white hover:bg-green-500"
>
<CubeTransparentIcon
v-if="isSubmitting"
class="h-5 w-5 animate-spin"
/>
<span v-else>Add Goal</span>
</button>
</div>
</form>
</div>
</template>
<script lang="ts">
import { CubeTransparentIcon } from "@heroicons/vue/outline";
import moment from "moment";
import { defineComponent, nextTick, ref } from "vue";
export default defineComponent({
components: {
CubeTransparentIcon,
},
props: {
title: {
type: String,
required: true,
},
dueDate: {
type: String,
required: true,
},
isSubmitting: {
type: Boolean,
default: false,
},
},
emits: ["update:title", "update:dueDate", "cancel", "submitting"],
setup(props, { emit }) {
const goalTextArea = ref(null as unknown as HTMLTextAreaElement);
const isVisibleGoalForm = ref(false);
const errors = ref([] as Array<string>);
function showGoalInput() {
isVisibleGoalForm.value = !isVisibleGoalForm.value;
nextTick(() => {
goalTextArea.value.focus();
});
}
function hideGoalInput() {
isVisibleGoalForm.value = false;
emit("cancel");
}
function submit(): void {
errors.value = [];
if (props.title === "" || props.dueDate === "") {
errors.value.push("Title & due date canot be empty");
return;
}
if (
moment(props.dueDate, "DD/MM/YYYY").format("DD/MM/YYYY") !==
props.dueDate
) {
errors.value.push("Due date must have a dd/mm/yyyy format");
return;
}
emit("submitting");
isVisibleGoalForm.value = false;
}
return {
goalTextArea,
isVisibleGoalForm,
errors,
showGoalInput,
hideGoalInput,
submit,
};
},
});
</script>
AppGoalForm.vue
component is a props can be bind with a v-model from a parent. In layment terms, v-model
is a combintation of binding a :value
attribute and emit events. <template>
<AppGoalForm
v-model:title="title"
v-model:dueDate="dueDate"
class="mt-8"
@submitting="submit()"
/>
</template>
AppGoalForm.vue
component now requiring a few additional props, it's time to make updates inside a Home.vue
, which will provide data to a child component as well as listen to emitted events.<template>
<BaseLayout>
<!-- the rest of template -->
<AppGoalForm
v-if="goalList.length < 2"
v-model:title="newGoal.title"
v-model:dueDate="newGoal.dueDate"
class="mt-8"
@submitting="addGoal()"
/>
</BaseLayout>
</template>
<script lang="ts">
// imports
export default defineComponent({
// components
setup() {
const store = useStore();
const { goalList } = store.state;
const newGoal = ref({
title: "",
dueDate: "",
} as Goal);
function addGoal() {
if (goalList.length < 2) {
store.commit("addGoal", newGoal.value);
newGoal.value = {
title: "",
dueDate: "",
} as Goal;
}
}
return {
// previously added
addGoal,
};
},
});
</script>
export const store = createStore<State>({
// state
mutations: {
// addGoal()
deleteGoal(state: State, goalId: string): void {
const index = state.goalList.findIndex((goal) => goal.id === goalId);
state.goalList.splice(index, 1);
},
},
});
delete
event and act upon it. <template>
<BaseLayout>
<template #header>
<h1 class="h-title">My Goals</h1>
</template>
<p class="text-sm italic text-gray-500">You can have up to 2 goals</p>
<ul class="mt-5 space-y-8">
<RouterLink v-slot="{ navigate }" to="/weekly-plan" custom>
<AppGoalListItem
v-for="goal in goalList"
:key="goal.id"
:goal="goal"
class="hover:bg-gray-50 cursor-pointer"
@click="navigate"
@delete="deleteGoal(goal.id)"
/>
</RouterLink>
</ul>
</BaseLayout>
</template>
<script lang="ts">
export default defineComponent({
components: {
AppGoalListItem,
},
setup() {
const store = useStore();
function deleteGoal(goalId: string): void {
store.commit("deleteGoal", goalId);
}
return {
deleteGoal,
};
},
});
</script>
export const store = createStore<State>({
// state
mutations: {
// add & delete operations
updateGoal(state: State, goalData: Goal): void {
const index = state.goalList.findIndex((goal) => goal.id === goalData.id);
state.goalList.splice(index, 1, goalData);
},
},
});
AppGoalForm.vue
components as it is the easiest one to update.<template>
<div>
<button
v-if="!isVisibleGoalForm && haveTriggerFormBtn"
class="py-10 border border-gray-200 rounded-md hover:bg-gray-50 w-full"
@click="showGoalInput()"
>
+ Add Goal
</button>
<form v-else class="space-y-2" @submit.prevent="submit()"
// form inputs
<div class="flex justify-end space-x-3">
// cancel button
<button
type="submit"
class="action-btn bg-green-400 text-white hover:bg-green-500"
>
<CubeTransparentIcon
v-if="isSubmitting"
class="h-5 w-5 animate-spin"
/>
<span v-else>{{ submitBtnText }}</span>
</button>
</div>
</form>
</div>
</template>
<script lang="ts">
// imports
export default defineComponent({
components: {
CubeTransparentIcon,
},
props: {
haveTriggerFormBtn: {
type: Boolean,
default: true,
},
submitBtnText: {
type: String,
default: "Add Goal",
},
},
emits: ["update:title", "update:dueDate", "cancel", "submitting"],
});
</script>
AppGoalListItem.vue
component, to handle a trigger for an edit form.<template>
<div>
<RouterLink
v-if="!isEditMode"
v-slot="{ navigate }"
:to="navigateTo"
custom
>
<li
class="p-10 border border-gray-200 rounded-md text-left"
:class="{ 'hover:bg-gray-50 cursor-pointer': isHoverable }"
@click="navigate()"
>
<div class="flex items-center justify-between mb-5">
// date
<div class="space-x-2 text-gray-400 -mt-5">
<button @click.stop="isEditMode = true">
<PencilIcon class="h-5 w-5 hover:text-gray-700" />
</button>
// delet button
</div>
</div>
// goal title
</li>
</RouterLink>
<AppGoalForm
v-else
v-model:title="toUpdateGoal.title"
v-model:dueDate="toUpdateGoal.dueDate"
submit-btn-text="Update"
class="mt-8 p-5 border border-gray-200 rounded-md text-left"
:have-trigger-form-btn="false"
@submitting="update()"
@cancel="cancel()"
/>
</div>
</template>
<script lang="ts">
// imports
export default defineComponent({
components: {
// icon components
AppGoalForm,
},
props: {
goal: {
type: Object as PropType<Goal>,
required: true,
},
navigateTo: {
type: String,
required: true,
default: "/",
},
isHoverable: {
type: Boolean,
default: false,
},
},
emits: ["update", "delete"],
setup(props, { emit }) {
function formatDate(date: string, format: string): string {
return moment(date, format, true).format(format);
}
const isEditMode = ref(false);
const toUpdateGoal = ref({ ...props.goal });
function update() {
emit("update", toUpdateGoal.value);
isEditMode.value = false;
}
function cancel() {
isEditMode.value = false;
toUpdateGoal.value = { ...props.goal };
}
return {
formatDate,
isEditMode,
toUpdateGoal,
update,
cancel,
};
},
});
</script>
AppGoalListItem.vue
component, so Home.vue
must be updated as well, otherwise, there will be a link wrapped around a link<template>
<BaseLayout>
// header and title
<ul class="mt-5 space-y-8">
<AppGoalListItem
v-for="goal in goalList"
:key="goal.id"
:goal="goal"
:navigate-to="`/${goal.id}/weekly-plan`"
:is-hoverable="true"
@update="updateGoal($event)"
@delete="deleteGoal(goal.id)"
/>
</ul>
// Add goal form
</BaseLayout>
</template>
<script lang="ts">
// imports
export default defineComponent({
components: {
AppGoalListItem,
AppGoalForm,
},
setup() {
const store = useStore();
// add & delete operations
function updateGoal(goal: Goal): void {
store.commit("updateGoal", goal);
}
return {
updateGoal,
};
},
});
</script>