Как решить проблему с логикой валидации после перезагрузки страницы в 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 шт):

Автор решения: yar85

После перезагрузки страницы в компоненте 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. Так или иначе, советую пересмотреть порядок "движения данных" в твоем проекте - потому что сейчас он слегка странно выглядит.

→ Ссылка