const items =[];
const dropdownItems = ref(items);
const dropdownItem = ref(null);
onMounted(() => {
ApiService.getStores().then((data) => {
data.forEach((value) => {
items.push({ name: value.shop,value:value.id });
});
});
});
<Dropdown id="state" v-model="dropdownItem" :options="dropdownItems" :filter="true"
optionLabel="name" optionValue="value"></Dropdown>
<p>{{ dropdownItem }}</p>
i am new in vue js
PrimeVue using here
{{ dropdownItem }} this working fine get value of selected drop down data.
but
1) how to set selected valued in localstroge
2)how to first data set default selected
Related
I'm trying to generate a confirmation modal when calling my composable, my component instance is mounting well, but I can't access the emits via the : onCancel
The goal is to call the dialer every time I need to interact with a confirmation
useModalConfirm.ts
function confirm(props: ModalConfirmProps) {
const container = document.createElement('div');
document.body.appendChild(container);
const component = createVNode(ModalConfirm, {
...props,
// not working here :(
onCancel: () => {
console.log('canceled')
}
});
render(component, container);
return component.component;
}
ModalConfirm.vue
<script lang="ts" setup>
import {NButton} from "naive-ui";
const emits = defineEmits(["onConfirm", "onCancel"]);
export type ModalConfirmProps = {
title: string;
message: string;
confirmButtonText: string,
cancelButtonText: string,
};
const props = defineProps<ModalConfirmProps>();
const confirm = () => {
emits("onConfirm");
};
const cancel = () => {
emits("onCancel");
};
</script>
<template>
<div class="ModalConfirm">
<div class="ModalConfirmContent">
{{ props.title }}
{{ props.message }}
<NButton #click="cancel" type="error">{{ props.cancelButtonText }}</NButton>
<NButton #click="confirm" type="success">{{ props.confirmButtonText }}</NButton>
</div>
</div>
</template>
any ideas ?
I have a local array with data which i pass to a table.
I created variables: page,limit,totalPage and function changePage.
In hook onMounted I calculate the number of pages based on the given limit.
Through the v-for loop, displayed the pages and made them clickable on the function, but I don't understand how to set the limit of displayed table elements, and how to make pagination work.
<template>
<div class="containerApp">
<Dialog
:showForm="showForm"
#changeToggle="changeToggle"
#hideDialog="hideDialog"
>
<PostForm #create="createPost" />
</Dialog>
<Select v-model="selectedSort" :options="sortOptions" />
</div>
<input type="text" v-model="searchQuery" />
<Table :tableData="searchAndSort" />
<div class="page_wrapper">
<div
v-for="(pageNumber, i) in totalPage"
:key="i"
#click="changePage(pageNumber)"
class="page"
:class="{ 'current-page': page === pageNumber }"
>
{{ pageNumber }}
</div>
</div>
</template>
<script>
import Table from "./components/Table.vue";
import Dialog from "./components/UI/Dialog.vue";
import PostForm from "./components/PostForm.vue";
import Select from "./components/UI/Select.vue";
import { ref } from "#vue/reactivity";
import { computed, onMounted } from "#vue/runtime-core";
export default {
name: "App",
components: {
...
},
setup() {
const showForm = ref(false);
const searchQuery = ref("");
const tableData = ref([
...
]);
const selectedSort = ref("");
const sortOptions = ref([
...
]);
const page = ref(1);
const limit = ref(5);
const totalPage = ref(0);
onMounted(() => {
totalPage.value = Math.ceil(tableData.value.length / limit.value);
});
const changePage = (pageNumber) => {
page.value = pageNumber;
};
const createPost = (post) => {
tableData.value.push(post);
showForm.value = false;
};
const changeToggle = (toggle) => {
showForm.value = !toggle;
};
const hideDialog = (val) => {
showForm.value = val;
};
const sortedPosts = computed(() => {
...
});
const searchAndSort = computed(() => {
...
});
return {
showForm,
tableData,
selectedSort,
sortOptions,
searchQuery,
changeToggle,
hideDialog,
createPost,
sortedPosts,
searchAndSort,
page,
limit,
totalPage,
changePage,
};
},
};
</script>
In order to show a portion of an array you can use Array.slice. (docs)
in your searchAndSort you should do something like this:
const searchAndSort = computed(() => {
const start = (page.value - 1) * limit.value;
const end = (page.value * limit.value)+1;
return tableData.value.slice(start, end);
});
page - 1 * limit - initially this will result in 0 * 5 meaning start from 0 (-1 is used because the page start from 1. When you go to the second page this will result in 1 * 5, etc.
end is defined by the limit itself multiplying the page.
So on the first page, you will slice the array from 0 to 5, on the second page - from 5 to 10, etc.
const end = (page.value * limit.value)+1; - +1 will give you the 5th element because Array.slice exlude the item at the end index)
You should add some checks as well.
I'm using Vue3 with the composition API. For this example, I will use Pinia but this applies to Vuex as well.
Imagine you have to take care of entity A holding entities of type entity B. For this example I will use a single todo holding multiple labels.
The sample store for the labels
export type Label = {
title: string; // "unique"
};
export const useLabelsStore = defineStore("labels", () => {
const labels = ref<Label[]>([]);
function addLabel(title: string): void {
if (!labels.value.some((label) => label.title === title)) {
labels.value.push({ title });
}
}
function deleteLabel(title: string): void {
labels.value = labels.value.filter((label) => label.title !== title);
}
return { labels, addLabel, deleteLabel };
});
and for the todos
export type Todo = {
title: string; // "unique"
labels: Label[];
};
export const useTodosStore = defineStore("todos", () => {
const todos = ref<Todo[]>([{ title: "foo", labels: [] }]);
function applyLabelToTodo(todoTitle: string, label: Label) {
const todo = todos.value.find((todo) => todo.title === todoTitle);
if (
todo &&
!todo.labels.some((todoLabel) => todoLabel.title === label.title)
) {
todo.labels.push(label);
}
}
return { todos, applyLabelToTodo };
});
You can create todos, labels and apply labels to todos. But after applying a label to a todo and deleting it afterwards the todo still holds it, the code has no sync mechanism yet, so the todos store is no "invalid" / "outdated".
This shows the App.vue file for testing purposes
<template>
<div>
<div>Labels: {{ labelsStore.labels }}</div>
<br />
<div v-for="todo in todosStore.todos" :key="todo.title">
<div>Todo: {{ todo.title }}</div>
<div>Applied labels: {{ todo.labels }}</div>
</div>
<br />
<button #click="addLabel">Add new label</button>
<br />
<br />
<select v-model="selectedLabelTitle">
<option
v-for="label in labelsStore.labels"
:value="label.title"
:key="label.title"
>
{{ label.title }}
</option>
</select>
<button #click="labelsStore.deleteLabel(selectedLabelTitle)">
Delete label "{{ selectedLabelTitle }}"
</button>
<button #click="applyLabelToTodo">
Apply label "{{ selectedLabelTitle }}" to todo "{{
todosStore.todos[0].title
}}"
</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { useTodosStore } from "./todosStore";
import { useLabelsStore } from "./labelsStore";
export default defineComponent({
setup() {
const todosStore = useTodosStore();
const labelsStore = useLabelsStore();
const selectedLabelTitle = ref<string>();
function addLabel(): void {
labelsStore.addLabel(`Label ${labelsStore.labels.length + 1}`);
}
function applyLabelToTodo(): void {
const label = labelsStore.labels.find(
(label) => label.title === selectedLabelTitle.value
);
if (label) {
todosStore.applyLabelToTodo(todosStore.todos[0].title, label);
}
}
return {
todosStore,
labelsStore,
addLabel,
selectedLabelTitle,
applyLabelToTodo,
};
},
});
</script>
The todos store already has a todo
Click on "Add new label"
Select it in the select box
Apply that label to the todo
Delete that label
You will see that this todo still holds the deleted label.
What is the common way to synchronize the stores? My current solution based on subscribing to actions is extending the todos store
export const useTodosStore = defineStore("todos", () => {
// ...
const labelsStore = useLabelsStore();
labelsStore.$onAction((context) => {
if (context.name === "deleteLabel") {
context.after(() => {
todos.value = todos.value.map((todo) => {
todo.labels = todo.labels.filter(
(label) => label.title !== context.args[0]
);
return todo;
});
});
}
});
// ...
});
Do I have to listen for all the actions or are there any better ways?
Factory function that defines a store and allows to define it with Vue reactive API directly is advanced feature that is generally not needed. A store is commonly defined with an object that is similar to Vuex API.
Values that are supposed to derive from other values are supposed to go to getters, very similarly to Vue computed values (computed is a part of Vue reactive API and used by Pinia internally).
There are many ways to do this, for instance, todo labels can be filtered against up-to-date labels from another store
const todoStore = defineStore({
state: () => ({ _todos: [] })
getters: {
todos() {
const labelStore = useLabelStore();
return this._todos
.map((todo) => ({
...todo,
labels: todo.labels.filter(label => labelStore.labels.includes(label))
});
}
}
});
I'm trying to use findComponent with find method to find a child component's element and set it's value. But every time I run test, it gives me Cannot call setValue on an empty DOMWrapper. error.
Test file
import { mount } from '#vue/test-utils';
import Create from './Create.vue';
// import State from '#/components/State.vue';
describe('it tests Create component', () => {
test('it emits create event and resets the form when form is valid and create button is clicked', async () => {
const div = document.createElement('div');
div.id = 'root';
document.body.append(div);
const expectedNameValue = 'TODO_NAME';
const expectedStateValue = 'Pending';
const wrapper = mount(Create, {
attachTo: '#root',
});
await wrapper.find(`input`).setValue(expectedNameValue);
await wrapper.findComponent({ ref: 'state-component' }).find('select').setValue(expectedStateValue);
await wrapper.find(`form`).trigger('submit');
expect(wrapper.emitted().create).toBeTruthy();
expect(wrapper.emitted().create[0]).toEqual([expectedNameValue]);
expect(wrapper.emitted().create[1]).toEqual(['Pending']);
expect(wrapper.find(`input[name='name']`).element.value).toEqual('');
expect(wrapper.find(`input[name='state']`).element.value).toEqual('Pending');
});
});
Create component
<template>
<form #submit.prevent="createTodo" class="flex gap-2 w-full">
<input class="flex-1 shadow rounded-md p-2 focus:ring-2 focus:ring-blue-900 focus:outline-none" type="text" placeholder="Todo Name" name="name" required/>
<State ref="state-component"/>
<button type="submit" class="rounded-md shadow text-white bg-blue-700 py-2 px-6">Create</button>
</form>
</template>
<script>
import State from '#/components/State.vue';
export default {
components: { State },
emits: ['create'],
methods: {
createTodo(event) {
const elems = event.target.elements;
const todo = { name: elems.name.value, state: elems.state.value };
this.$emit('create', todo);
elems.name.value = '';
elems.state.value = 'Pending';
}
}
}
</script>
<style scoped>
</style>
State component
<template>
<select id="state-select" class="rounded-md bg-green-200 text-white" name="state">
<option
v-for="(state, index) in states"
:selected="isSelected(state)"
:key="index"
>
{{ state }}
</option>
</select>
</template>
<script>
export default {
props: ["todo", "index"],
data() {
return {
name: "",
state: "",
states: ["Pending", "In Progress", "Done"],
};
},
created() {
if(!this.todo) return true;
this.state = this.todo.state;
this.name = this.todo.name;
},
methods: {
isSelected(equivalent){
return equivalent === this.state;
}
}
};
</script>
<style scoped></style>
I'm fairly new to VueJS so I'm open to all tips and tricks, thanks.
Some issues to fix:
You don't need to attach the component to the document, so remove that:
// ❌
// const div = document.createElement('div');
// div.id = 'root';
// document.body.append(div);
// const wrapper = mount(Create, { attachTo: '#root' });
// ✅
const wrapper = mount(Create);
The template ref to the State component would be the component's root element, so no need to find() the <select>:
// ❌
// await wrapper.findComponent({ ref: 'state-component' }).find('select').setValue(expectedStateValue);
^^^^^^^^^^^^^^^
// ✅
await wrapper.findComponent({ ref: 'state-component' }).setValue(expectedStateValue);
The emitted() object key is the event name, and the value is an array of of arrays, containing emitted data. You can verify the first create-event data contains another object with toMatchObject(object):
// ❌
// expect(wrapper.emitted().create[0]).toEqual([expectedNameValue]);
// expect(wrapper.emitted().create[1]).toEqual(['Pending']);
// ✅
expect(wrapper.emitted().create[0][0]).toMatchObject({ name: expectedNameValue, state: 'Pending' });
The last assertion tries to find input[name='state'], but that's actually a <select>, not an <input>:
// ❌
// expect(wrapper.find(`input[name='state']`).element.value).toEqual('Pending')
^^^^^
// ✅
expect(wrapper.find(`select[name='state']`).element.value).toEqual('Pending')
demo
I am getting an array of objects from firebase and showing them characters component in list format using V-for. Everytime I go to homepage and returning to characters page the list are getting multiplied and showing me duplicate keys.
characters.vue:
<template>
<ul class="characters-list">
<li v-for="allHero in getAllHeros" v-bind:key="allHero.id">
<router-link :to="{ name: 'characterDetail', params: { id: allHero.id } }">
<div class="hero-thumbnail">
<img :src="allHero.imageUrl" :alt="allHero.title" />
</div>
<div class="hero-detail">
<h3>{{ allHero.name }}</h3>
</div>
</router-link>
</li>
</ul>
</template>
import database from "#/firebase/init";
computed: {
...mapGetters({ getAllHeros: "getAllHeros" }),
},
created() {
database
.collection("heros")
.get()
.then(snapshot => {
snapshot.forEach(doc => {
let heros = doc.data();
heros.id = doc.id;
this.$store.dispatch('fetchAllHeros', heros)
});
});
}
VUEX Module -
const state = {
allHeros: []
};
const getters = {
getAllHeros: state => {
return state.allHeros;
}
};
const actions = {
async fetchAllHeros({ commit }, heros) {
commit("setAllHeros", heros);
}
};
const mutations = {
setAllHeros: (state, payload) => {
state.allHeros.push(payload);
}
};
When you route to a new page your Vuex store does not necessarily get reset to its initial state. Therefore every time that component is created you are adding more heros to the vuex store which is resulting in duplicate heros being added.
To prevent this, you can just use some simple logic to check if any heroes have been loaded:
created() {
if(this.getAllHeros.length == 0){
//Get heros from database..
};
}