Как использовать Pinia с картой объектов конфигурации таблицы

Я использую Pinia для управления состоянием различных таблиц данных в моем приложении. Несколько таблиц могут быть открыты «поверх» друг друга (т. е. в наложении друг на друга). Вот почему я переделываю нашу текущую настройку, в которой можно открыть только одну таблицу, а другая таблица перезаписывает предыдущую, в новую систему, в которой одновременно можно открыть несколько таблиц.

Я подумал об использовании Pinia с Map(), чтобы сохранить все конфигурации таблиц данных на месте, и использовать поле «activeDataTableName», чтобы отслеживать, какая таблица в данный момент является «активной».

Мой магазин Пинии выглядит так:

export const useDataTableStore = defineStore('data-table', {
  state: () => ({
    dataTables: new Map(), // use a Map to store all data table configurations
    activeDataTableName: null, // Used to track which is the current active data table
  }),
  getters: {
    getActiveDataTableConfig: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig || null
    },
    getDataTableName: (state) => state.activeDataTableName,
    getLoading: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig ? dtConfig.loading : true
    },
    getDataTableColumns: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig ? dtConfig.dataTableColumns : []
    },
    getTotalRecords: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig ? dtConfig.totalRecords : 0
    },
    getFirst: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig ? dtConfig.first : 0
    },
    getAmountRowsToShow: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig ? dtConfig.amountRowsToShow : 15
    },
    getSelectedRows: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig ? dtConfig.selectedRows : []
    },
    getFocussedRow: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig ? dtConfig.focussedRow : null
    },
    getLazyParams: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig ? dtConfig.lazyParams: {}
    }
    getSelectAll: (state) => {
      const dtConfig = state.dataTables.get(state.activeDataTableName)
      return dtConfig ? dtConfig.selectAll : false
    },
  },
  actions: {
    async initialize (nuxtApp, dtName) {
      // The initialize function sets the dataTableName and loads the columns
      const newDataTableConfigObject = {
        loading: true,
        dataTableName: dtName,
        dataTableColumns: [],
        totalRecords: 0,
        first: 0,
        amountRowsToShow: 15,
        selectedRows: [],
        focussedRow: null,
        lazyParams: {},
        selectAll: false,
      }
      this.dataTables.set(dtName, newDataTableConfigObject)
      this.activeDataTableName = dtName
    },
    setSelectedRows (rows) {
      const dtConfig = this.dataTables.get(this.activeDataTableName)
      if (dtConfig) {
        dtConfig.selectedRows = rows
      }
    },
  },
})

Мои проблемы возникают, когда я пытаюсь выполнить двустороннюю привязку при использовании v-модели. Например: у меня будет компонент «DataTable», который принимает значения из моего магазина в v-модели следующим образом:

<DataTable
        ref = "dataTableRef"
        v-model:selection = "store.selectedRows"
        v-model:first = "store.first"
        v-model:rows = "store.amountRowsToShow"
        ...
/>

когда мой магазин инициализируется:

const store = useDataTableStore()

значения «selectedRows», «first» и т. д. будут находиться не непосредственно в хранилище, а в активном объекте конфигурации внутри dataTables в хранилище. Я также пытаюсь установить значения для переменных магазина:

store.amountRowsToShow = rows
store.lazyParams.first = store.getFirst
store.lazyParams.rows = store.getAmountRowsToShow

Вероятно, это можно решить с помощью специальных установщиков для каждого из них, но если мне придется создавать метод установки для каждого потенциального объекта внутри lazyParams, это становится очень утомительно. Нет ли лучшего подхода к этому?


ОБНОВЛЕНИЕ: я изменил магазин по рекомендации @Estus Flask. Сейчас это выглядит так:

import { defineStore, acceptHMRUpdate } from 'pinia'
import {
  returnDataTableDefaultColumnsFullInfo,
  returnDataTableColumnsFullInfoForFields,
  returnDataTableAllColumns,
} from 'give-a-day-shared/data/dataTableConfig.mjs'
import { ResponseError } from 'give-a-day-shared/routes/responseUtils.mjs'
import { isSuperAuthor } from '../functions/frontendAuthUtils.js'

function createEmptyDataTableConfig (dtName) {
  return {
    showDataTable: false,
    loading: true,
    dataTableName: dtName,
    allDataTableColumns: [],
    dataTableColumns: [],
    dataVisibilitySelectedOption: null,
    dataVisibilityPlatformOptionVisible: null,
    dataVisibilityUmbrellaOptionVisible: null,
    totalRecords: 0,
    first: 0,
    amountRowsToShow: 15,
    selectedRows: [],
    focussedRow: null,
    selectAll: false,
    lazyParams: {}, // This variable is used to send to the backend
    previewPanelVisible: true,
  }
}

function setNestedProperty (obj, path, value) {
  const keys = path.split('.')
  let current = obj

  for (let i = 0; i < keys.length - 1; i++) {
    if (!current[keys[i]]) {
      current[keys[i]] = {}
    }
    current = current[keys[i]]
  }

  current[keys[keys.length - 1]] = value
}

export const useDataTableStore = defineStore('data-table', {
  state: () => ({
    dataTables: new Map(), // use a Map to store all data table configurations
  }),
  getters: {
    // BIG NOTE: Parameterized getters are NOT cached.
    // This is why we create helpers that create computed's that use these getters to get the caching back.
    getShowDataTable: (state) => (dtName) => state.dataTables.get(dtName)?.showDataTable || false,
    getLoading: (state) => (dtName) => state.dataTables.get(dtName)?.loading || true,
    getAllDataTableColumns: (state) => (dtName) => state.dataTables.get(dtName)?.allDataTableColumns || [],
    getDataTableColumns: (state) => (dtName) => state.dataTables.get(dtName)?.dataTableColumns || [],
    getDataVisibilitySelectedOption: (state) => (dtName) => state.dataTables.get(dtName)?.dataVisibilitySelectedOption || null,
    getDataVisibilityPlatformOptionVisible: (state) => (dtName) => state.dataTables.get(dtName)?.dataVisibilityPlatformOptionVisible || null,
    getDataVisibilityUmbrellaOptionVisible: (state) => (dtName) => state.dataTables.get(dtName)?.dataVisibilityUmbrellaOptionVisible || null,
    getTotalRecords: (state) => (dtName) => state.dataTables.get(dtName)?.totalRecords || 0,
    getFirst: (state) => (dtName) => state.dataTables.get(dtName)?.first || 0,
    getAmountRowsToShow: (state) => (dtName) => state.dataTables.get(dtName)?.amountRowsToShow || 15,
    getSelectedRows: (state) => (dtName) => state.dataTables.get(dtName)?.selectedRows || [],
    getFocussedRow: (state) => (dtName) => state.dataTables.get(dtName)?.focussedRow || null,
    getSelectAll: (state) => (dtName) => state.dataTables.get(dtName)?.selectAll || false,
    getLazyParams: (state) => (dtName) => state.dataTables.get(dtName)?.lazyParams || {},
    getPreviewPanelVisible: (state) => (dtName) => state.dataTables.get(dtName)?.previewPanelVisible || true,
  },
  actions: {
    async initialize (nuxtApp, dtName) {
      // The initialize function sets the dataTableConfig and loads the columns
      this.resetConfig(dtName)
      await this.fetchDataTableColumns(nuxtApp, dtName)
    },
    resetConfig (dtName) {
      const newConfig = {
        showDataTable: false,
        loading: true,
        dataTableName: dtName,
        allDataTableColumns: [],
        dataTableColumns: [],
        dataVisibilitySelectedOption: null,
        dataVisibilityPlatformOptionVisible: null,
        dataVisibilityUmbrellaOptionVisible: null,
        totalRecords: 0,
        first: 0,
        amountRowsToShow: 15,
        selectedRows: [],
        focussedRow: null,
        selectAll: false,
        lazyParams: {}, // This variable is used to send to the backend
        previewPanelVisible: true,
      }
      this.dataTables.set(dtName, newConfig)
    },
    async fetchInitialDataToShowState (nuxtApp, dtName, hideDataToShowOptionPlatform = false, hideDataToShowOptionUmbrella = false) {
      if (!isSuperAuthor(nuxtApp.$auth.user)) {
        let dataTableConfig = this.dataTables.get(dtName)
        if (!dataTableConfig) {
          throw new ResponseError('no_data_table_config_found')
        }

        if (nuxtApp.$getPlatformOfUser() && !hideDataToShowOptionPlatform) {
          let platform = await nuxtApp.$getFromBackend('/fetchPlatformById', { platformId: nuxtApp.$getPlatformOfUser().id })
          if (platform) {
            dataTableConfig.dataVisibilityPlatformOptionVisible = true
            dataTableConfig.dataVisibilitySelectedOption = 'show_all'
          }
        }

        if (nuxtApp.$globalState.toggledOrganisationId && !hideDataToShowOptionUmbrella) {
          let umbrellaOrgIds = await nuxtApp.$getFromBackend('/fetchAllOrganisationIdsInTree', { organisationId: nuxtApp.$globalState.toggledOrganisationId, includeInvites: false })
          if (umbrellaOrgIds.length > 1) {
            dataTableConfig.dataVisibilityUmbrellaOptionVisible = true
            if (!dataTableConfig.dataVisibilitySelectedOption) {
              dataTableConfig.dataVisibilitySelectedOption = 'show_only_umbrella'
            }
          }
        }

        if (!dataTableConfig.dataVisibilitySelectedOption) {
          dataTableConfig.dataVisibilitySelectedOption = 'show_only_myself'
        }
      }
    },
    async fetchDataTableColumns (nuxtApp, dtName) {
      let dataTableConfig = this.dataTables.get(dtName)
      if (!dataTableConfig) {
        throw new ResponseError('no_data_table_config_found')
      }
      let allTableColumns = returnDataTableAllColumns(nuxtApp, dtName) // we use this for mapping the filter query params
      if (allTableColumns) {
        dataTableConfig.allDataTableColumns = allTableColumns
      }
      let existingTableOrgColumnsConfig = await nuxtApp.$getFromBackend('/fetchDataTableColumnConfiguration', { dataTableName: dataTableConfig.dataTableName })
      if (existingTableOrgColumnsConfig) {
        // Use the custom config of the organisation
        let columnData = existingTableOrgColumnsConfig.columnData
        if (typeof columnData === 'string') {
          columnData = JSON.parse(columnData)
        }
        let columnsFullInfo = returnDataTableColumnsFullInfoForFields(nuxtApp, dataTableConfig.dataTableName, columnData)
        dataTableConfig.dataTableColumns = columnsFullInfo
      } else {
        // Use the default config
        let defaultColumnsFullInfo = returnDataTableDefaultColumnsFullInfo(nuxtApp, dataTableConfig.dataTableName)
        dataTableConfig.dataTableColumns = defaultColumnsFullInfo
      }
    },
    setLoading (dtName, bool) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.loading = bool
      }
    },
    setShowDataTable (dtName, bool) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.showDataTable = bool
      }
    },
    setSelectedRows (dtName, rows) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.selectedRows = rows
      }
    },
    setFirst (dtName, first) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.first = first
      }
    },
    setAmountRowsToShow (dtName, amountRowsToShow) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.amountRowsToShow = amountRowsToShow
      }
    },
    setTotalRecords (dtName, amount) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.totalRecords = amount
      }
    },
    setDataVisibilitySelectedOption (dtName, dataToShow) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.dataVisibilitySelectedOption = dataToShow
      }
    },
    setSelectAll (dtName, bool) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.selectAll = bool
      }
    },
    setFocussedRow (dtName, row) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.focussedRow = row
      }
    },
    setLazyParams (dtName, lazyParams) {
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        dtConfig.lazyParams = lazyParams
      }
    },
    setLazyParamsProperty (dtName, propertyPath, value) {
      // The propertyPath is just a string value (with a . if it's a nested property) (eg: 'filters.organisationName')
      const dtConfig = this.dataTables.get(dtName)
      if (dtConfig) {
        setNestedProperty(dtConfig.lazyParams, propertyPath, value)
      }
    },
  },
})

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useDataTableStore, import.meta.hot))
}

И когда магазин Pinia загружается в мой компонент:

const dtStore = useDataTableStore()

Я получаю следующую ошибку:

[nuxt] [request error] [unhandled] [500] Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property '_context' -> object with constructor 'Object'
    --- property 'app' closes the circle
  --> starting at object with constructor 'Object'
  at JSON.stringify (<anonymous>)

Кажется, это происходит только при принудительном обновлении страницы (F5), а не при обычном переходе на страницу.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
82
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Наличие activeDataTableName в хранилище — это нормально, хотя это состояние может принадлежать «активному» компоненту, а не хранилищу.

Существует фундаментальная проблема с геттерами типа getActiveDataTableConfig. Если предполагается, что несколько таблиц будут отображаться одновременно, будет невозможно использовать геттеры для отображения данных в неактивных таблицах. Потенциально было бы меньше проблем, если бы все таблицы в хранилище рассматривались как равные. Для этого можно использовать параметризованные вычисления:

getters: {
  getSelectedRows: (state) => {
    return (name) => state.dataTables.get(name) || null;
  },
  ...

Параметризованные вычисления не кэшируются, это может быть критично для ресурсоемкого использования, которым может быть таблица. Вместо или в дополнение к параметризованным вычислениям можно использовать помощник, который создает вычисления для конкретной таблицы:

function mapStoreTable(name) {
  const store = useTableStore();
  const tableData = store.dataTables.get(name);
  
  const selectedRows = computed(() => store.getSelectedRows(name));
  ...
  return { selectedRows, ... };
}

Это заставит selectedRows и т. д. не реагировать на изменения в других таблицах.

Действие обновления может принимать имя таблицы для обновления определенной таблицы:

  actions: {
    setSelectedRows(name, rows) {
      const dtConfig = this.dataTables.get(name);
      if (dtConfig) {
        dtConfig.selectedRows = rows
      }
    },
    ...

Мутировать магазин напрямую с помощью v-model вообще сомнительная практика. Хотя это разрешено в Pinia, ранее в Vuex это не поощрялось, поскольку в результате получается код, который трудно отлаживать и поддерживать. В любом случае это было бы невозможно с предложенным помощником. v-model можно обесцветить для реквизита и мероприятия. Или записываемый вычисляемый объект, который отправляет действие на набор и может использоваться как v-model:selection = "selectedRowsModel":

const { selectedRows } = mapStoreTable(tableName);

const selectedRowsModel = computed({
  get() {
    return selectedRows.value;
  },
  set(rows) {
    store.setSelectedRows(tableName, rows) 
  }
});

Спасибо за ваш ответ! На данный момент это очень помогает. Мне было интересно, где мне определить вспомогательную функцию mapStoreTable(name)? Это внутри моего компонента с данными? А также в отношении сбора мусора, кэширования и прочего. Есть ли какие-либо предостережения, о которых мне следует знать при ваших предложениях?

Dennis 02.07.2024 06:47

И согласно вашему примеру во вспомогательной функции, для чего мне использовать переменную tableData?

Dennis 02.07.2024 08:50

Помощник переходит в отдельный .js-модуль. Он похож на помощника storeToRefs от Pinia. «tableData» — это состояние хранилища для одной таблицы. Вероятно, оно вам не понадобится, если вся логика хранилища хранится в геттерах и действиях. Используйте mapStoreTable в определенном компоненте в «настройке» для вывода определенной таблицы, когда имя уже известно. DataTable или его родительский элемент могут принять имя таблицы в качестве реквизита для связи компа с хранилищем. Никаких конкретных проблем, о которых я не мог подумать

Estus Flask 02.07.2024 10:42

Это так ясно! Большое спасибо!! Я уже отметил ответ как принятый. Однако я столкнулся с проблемой, из-за которой мой магазин Pinia выдает ошибку «Преобразование круговой структуры в JSON». Я изменил вопрос выше и добавил полностью переработанное определение магазина. Кажется, я не могу найти, в чем проблема. Это происходит только при нажатии CTRL+F5 для принудительного обновления страницы. При переходе на страницу без жесткого обновления кажется, что все работает нормально.

Dennis 02.07.2024 11:44

Похоже, это очень специфично для Nuxt и может быть сложно отладить. Похоже, он пытается сериализовать экземпляр приложения Vue в JSON на стороне сервера, что не имеет смысла. Магазин не имеет прямого отношения к проблеме. Ближайшая вещь, которая может косвенно вызвать это, - это горячая перезагрузка, попробуйте отключить import.meta.hot.accept... Также я бы предложил изменить процедуру инициализации, поскольку она, возможно, связана с жизненным циклом, проверьте, совпадает ли она, если вы это сделаете 'не вызываю инициализацию(), а заполняю ее поддельными данными. Также проверьте, происходит ли это в режиме prod.

Estus Flask 02.07.2024 12:13

Другие вопросы по теме