当使用ngrx在多个选项卡(在应用程序级别)可以打开功能时,如何隔离功能状态?

时间:2019-04-22 14:04:56

标签: angular tabs ngrx ngrx-store

我必须使用ngrx(要求)构建具有以下要求的应用程序:

  • 多种功能,我们称它们为A,B和C。
  • 在应用程序级别的选项卡。选项卡包含功能,并且可以打开的选项卡数量没有限制。
  • 可以在用户想要的任意多个选项卡中打开功能。
  • 选项卡状态必须与其他选项卡隔离,即使它包含相同的功能。

最后一点是我被困住的那一刻,我正在为此而严重失去睡眠,我想让我的生活恢复原状。有没有人对如何做这样的事情有经验?

到目前为止,我所做的是构建一个示例应用程序。经过数天的忙碌之后,我找到了一个解决方案,但是我觉得这很难维护。我要解决的办法是:

  • 将包含在标签中的功能必须具有状态(例如FeatureAState
  • 此状态将存储在该功能的状态图(例如FeatureAStates)中,其键为该状态所属标签的ID。
  • 标签导航完成后,我必须在所有功能中收听此操作以更新其状态以了解当前标签。
  • 然后,我们使用选择器来获取当前活动标签页的功能状态。

但是我在解决方案上遇到的问题是:

1)每个功能都必须意识到它们位于选项卡中,并且我必须在每个功能中都复制selectedTabId(并且不要忘了每次用户浏览选项卡时都要对其进行更新)。 2)所有子功能必须包装在states: {[tabIdOfThisState: string]: SubFeatureState}中,这使访问属性很麻烦。

我如何改善此解决方案?

下面您可以找到代码的样子。

非常感谢您提供的任何帮助,上帝知道我需要一些帮助。

tabs.actions.ts

// Tab is an interface with an id, a matching url and a label.
export const addNewTab = createAction('[Tabs] Add New Tab', props<{ tab: Tab}>());
export const navigateToTab = createAction('[Tabs] Navigate To Tab', props<{ tab: Tab }>());
export const closeTab =  = createAction('[Tabs] Close Tab', props<{ tab: Tab }>());
export const all = union({ addNewTab, navigateToTab });
export type TabsActionsUnion = typeof all;

tabs.reducer.ts

export interface State extends EntityState<Tab> { 
  selectedTabId: string;
}

export const adapter: EntityAdapter<Tab> = createEntityAdapter<Tab>({
  selectId: (tab: Tab) => tab.linkOrId,
});

export const initialState: State = adapter.getInitialState({ selectedTabId: undefined });

export function reducer(state = initialState, action: TabsActionsUnion) {
  switch (action.type) {
    case addNewTab.type: {
      return adapter.addOne(action.tab, state);
    }
    case navigateToTab.type: {
      return { ...state, selectedTabId: action.tab.id };
    }
    case closeTab.type: {
      // (todo): should handle the case where the tab being removed is the selected one
      return adapter.removeOne(action.tab.id, state);
    }
    default: {
      return state;
    }
  }
}

export const getSelectedTabId = (state: State) => state.selectedTabId;

然后我有了我的功能A。我的功能A处理了多个子功能,在示例应用程序中,我有一个名为Counter的功能。代码如下所示:

counter.actions.ts

export const increaseCounter = createAction('[Counter] Increase Counter', props<{ tabId }>());
export const decreaseCounter = createAction('[Counter] Decrease Counter');
const all = union({ initializeCounter, increaseCounter, decreaseCounter });
export type CounterActions = typeof all;

counter.reducer.ts

export interface CounterState {
  value: number; 
}

// this is the list of the different states, one per tab where the counter feature is used... 
export interface CounterStates {
  states: { [id: string]: CounterState };
  activeTabId: string; // this is the selected tab id, this is always the same one as the selectedTabId in the Tabs feature 
}

const initialStates = { states: {}, activeTabId: undefined };

const initialState = { value: 0 };

export function reducer(state: CounterStates = initialStates, action: CounterActions | TabsActionsUnion) {
  switch (action.type) {
    // when we add a new tab we need to initialize a new counter state for this tab. 
    // this also means that we cannot lazy load the store, because otherwise 
    // this reducer would not be initialized on the first addNewTab
    case addNewTab.type: {
      return {
        ...state,
        states: { ...state.states, [action.tab.id]: initialState },
        activeTabId: action.tab.id
      };
    }
    // we have to duplicate the activeTabId here because the reducer cannot access the
    // state of another feature...
    case navigateToTab.type: {
      return {
        ...state,
        activeTabId: action.tab.id
      };
    }
    // updating the value is painful because we need to make sure we modify only the right counter state...
    case increaseCounter.type: {
      return {
        ...state,
        states: {
          ...state.states,
          [action.tabId]: { ...state.states[action.tabId], value: state.states[action.tabId].value + 1 }
        }
      };
    }
    case decreaseCounter.type: {
      return {
        ...state,
        states: {
          ...state.states,
          [state.activeTabId]: { ...state.states[state.activeTabId], value: state.states[state.activeTabId].value - 1 }
        }
      };
    }
    default: {
      return state;
    }
  }
}

// selectors are ok to work with, this one I'm happy with
export const getValue = (state: CounterStates) => state.states[state.activeTabId].value;

以及使用Counter的整个功能的化简器:

index.ts

export interface FeatureAState {
  counter: fromCounter.CounterStates;
}

export interface State extends fromRoot.State {
  featureA: FeatureAState;
}

export const reducers = combineReducers({ counter: fromCounter.reducer });

export const getFeatureAStateState = createFeatureSelector<State, FeatureAStateState>('featureAState');

export const getFeatureAStateCounterState = createSelector(
  getFeatureAStateState,
  state => state.counter
);

export const getCounterValue = createSelector(
  getFeatureAStateState,
  fromCounter.getValue
);

1 个答案:

答案 0 :(得分:0)

我相信您使用的是“正确的”解决方案。您将必须维护每个选项卡的状态,这只能通过保留对其ID的引用来完成,这就是您正在做的事情。

我相信(通过url)为活动标签ID选择器是一种干净的解决方案