Как решить проблему с логикой валидации после перезагрузки страницы в Vue.js?
Есть форма с двумя полями. В компоненте TheEducation.vue находится бизнес-логика и логика валидации полей, а в компоненте BaseForm.vue происходит отрисовка полей и сохранение в локальное хранилище. Суть в том, что изначально шаблон работает с массивом propList, который я передаю из TheEducation в BaseForm, и поля "errorMessages":"error"
получают ошибки по необходимости. Всё работает. После перезагрузки страницы в компоненте BaseForm отрабатывает уже ветка else:
if (!storedList) {
list.value = propList;
listType.value = 'propList: ' + JSON.stringify(list.value);
} else {
list.value = storedList;
listType.value = 'storedList: ' + JSON.stringify(list.value);
}
Соответственно, шаблон работает уже с данными из локального хранилища, и функция getValidatePanels уже не присваивает массиву значение свойства errorMessages. В чем может быть проблема? Я полагаю, что возможно функция getValidatePanels после перезагрузки страницы работает не с тем массивом. Во всяком случае, буду очень благодарен, если поможете разобраться. Это мой давний висяк, и я не могу решить его.
Песочница: https://replit.com/@teplandrey41/ValidationForm#src/components/TheEducation.vue
Код: Родитель
<template>
<BaseForm
@blurEvent="handlePanelBlurEvent"
:personalDetailsPanelArr="personalDetailsPanelArr"
:educationPanelInputsArr="educationPanelInputsArr"/>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import BaseForm from './BaseForm.vue';
const educationPanelInputsArr = ref([
{ id: Math.floor(Math.random() * 100), name: 'InstituteName', errorMessages: '', validationData: 'EducationInstituteName', label: 'Name of educational institution', type: 'text', value: '', isEducationName: true },
{ id: Math.floor(Math.random() * 100), name: 'YearOfEntry', errorMessages: '', validationData: 'EducationYearOfEntry', label: 'Year of entry', type: 'number', value: ''}
]);
const personalDetailsPanelArr = ref([
{
id: Math.floor(Math.random() * 100),
type: 'Education',
title: 'Education',
isOpen: false,
inputs: educationPanelInputsArr.value,
}
]);
//"запрос на сервер" для получения валидации по полям
const getValidatePanels = () => {
let storedDataPanels = ref(JSON.parse(localStorage.getItem('personalDetailsEducationPanels')) || []);
//формирую объект с полями, value которых нужно будет провалидировать (отправить на сервер)
const newInitialPersonalDetailsListArray = storedDataPanels.value.map(item => {
const details = {};
item.inputs.forEach(input => {
details[input.validationData] = input.value;
});
return details;
});
//функция для присваивания возвращенных ошибок валидации с сервера
const extractDataFromPanels = (panel) => {
panel.forEach(inputField => {
const fieldName = inputField.validationData;
const foundInput = panel.find(input => input.validationData === fieldName);
if (foundInput.value.trim() === '') {
foundInput.errorMessages = 'error';
} else {
foundInput.errorMessages = '';
}
});
}
extractDataFromPanels(educationPanelInputsArr.value);
}
const handlePanelBlurEvent = () => {
getValidatePanels();
}
onMounted(() => {
getValidatePanels()
});
</script>
Потомок
<template>
<h1>{{ computedURLTitle }}</h1>
<div
v-for="(input, index) in allPanels" :key="index">
<hr>
<div
v-for="(inputField, inputIndex) in input.inputs"
:key="inputIndex">
<BaseInput
v-model="inputField.value"
:type="inputField.type"
:label="inputField.label"
@blur="$emit('blurEvent')"
/>
<div style="color: red;"> {{inputField.errorMessages}} </div>
</div>
</div>
<button @click="handleAddPanel">add</button>
<div style="color: blue;">{{ listType }}</div>
</template>
<script setup>
import { ref, defineProps, defineEmits, computed, watch } from 'vue';
import BaseInput from './BaseInput.vue';
import { useRouter } from 'vue-router';
const emits = defineEmits(['blurEvent']);
const router = useRouter();
const currentURL = ref(router.currentRoute.value.path);
const educationURL = '/education';
let listType = ref(null)
const props = defineProps({
personalDetailsPanelArr: {
type: Array,
},
educationPanelInputsArr: {
type: Array,
},
});
const pathTitles = {
'/education': 'Education',
};
const computedURLTitle = computed(() => {
const currentPath = currentURL.value;
return pathTitles[currentPath];
});
console.log('personalDetailsPanelArr: ', props.personalDetailsPanelArr)
let personalDetailsList = ref([]);
const storedPersonalDetailsList =
JSON.parse(localStorage.getItem('personalDetailsEducationPanels'));
if (storedPersonalDetailsList) {
personalDetailsList.value = storedPersonalDetailsList;
}
const updateLocalStorage = () => {
localStorage.setItem('personalDetailsEducationPanels', JSON.stringify(personalDetailsList.value));
}
const watchInput = (panel) => {
panel.inputs.forEach((input) => {
watch(input, (newValue) => {
if (newValue.isEducationName) {
panel.title = newValue.value || 'Education';
}
updateLocalStorage();
});
});
};
const panelLists = {
'/education': {
list: personalDetailsList,
storedList: storedPersonalDetailsList,
propList: props.personalDetailsPanelArr,
},
};
const allPanels = computed(() => {
const currentList = panelLists[currentURL.value];
if (currentList) {
const { list, storedList, propList } = currentList;
if (!storedList) {
list.value = propList;
listType.value = 'propList: ' + JSON.stringify(list.value);
} else {
list.value = storedList;
listType.value = 'storedList: ' + JSON.stringify(list.value);
}
list.value.forEach((panel) => {
watchInput(panel);
});
return list.value;
}
return [];
});
</script>
Ответы (1 шт):
После перезагрузки страницы в компоненте BaseForm .... шаблон работает уже с данными из локального хранилища
Потому что программного удаления/сопоставления данных в localStorage
не выполняется => после первой же записи, !storedList
всегда будет false
до тех пор пока юзер данные вручную не сотрет (таким образом, всегда будет использоваться состояние считанное из LS, игнорируя переданное через пропсы).
В части состояния дочернего компонента, задачу с реализованным решением я вообще не понял из предоставленного кода (который имхо выглядит абсолютно рандомным и нелогичным) - но если нужно постоянно сохранять в LS внутренний стейт дочернего компонента, подтягивая в него внешние данные из пропсов по мере их изменения, то это можно например вот так реализовать:
console.log('personalDetailsPanelArr: ', props.personalDetailsPanelArr)
// все что было ниже этой⤴ строки - фтопку, и вместо удаленного:
const personalDetailsList = reactive([]);
const updateLocalStorage = list => {
console.info('Updating data stored in LS.'); // как индикатор :)
list ??= personalDetailsList;
localStorage.setItem('personalDetailsEducationPanels', JSON.stringify(list));
return list;
}
const panelLists = computed(() => ({
'/education': personalDetailsList,
}));
const allPanels = computed(() => unref(panelLists)[currentURL.value] ?? []);
watch(props.personalDetailsPanelArr, newVal => {
Object.assign(personalDetailsList, newVal);
});
watch(personalDetailsList, newVal => {
for (const panel of newVal) {
const eduInp = panel.inputs.find(inp => inp.isEducationName);
if (eduInp)
panel.title = eduInp.value.trim() || 'Education';
}
updateLocalStorage();
});
Первый watch
следит за изменением входных данных от родительского компонента, а второй отвечает за запись в LS состояния при изменениях внутри personalDetailsList
(с объектами обернутыми в прокси через reactive
, по-умолчанию применяется глубокое отслеживание).
Но вот зачем сохранять в LS данные, т.е. когда их предполагается считывать (при том что внутренний стейт компонента по определению из внешнего инициализируется, через props
) и нужно ли это считывание в целом - я ваще не понял ?
По идее, за сохранение и восстановление данных ответственен их источник... в данном случае, это родительский компонент. Если очень сильно надо именно в дочерних компонентах изменять данные из родительского состояния (вместо порождения событий как предписывает "vue way"), то вариантом может стать использование vuex/pinia. Так или иначе, советую пересмотреть порядок "движения данных" в твоем проекте - потому что сейчас он слегка странно выглядит.