27 votes

Angulaire 2, ngrx/magasin, RxJS et arborescente de données

J'ai essayé de trouver un moyen d'utiliser la sélection de l'opérateur en combinaison avec rxjs autres opérateurs de requête d'une structure d'arbre de données (normalisé dans le magasin pour une liste à plat), de telle sorte qu'il préserve l'intégrité référentielle pour ChangeDetectionStrategy.OnPush sémantique, mais mes tentatives de cause la totalité de l'arbre pour être réaffichées lorsqu'une partie des modifications apportées à l'arbre. Quelqu'un aurait-il des idées? Si vous considérez l'interface suivante en tant que représentant de données dans le magasin:

export interface TreeNodeState {
 id: string;
 text: string;
 children: string[] // the ids of the child nodes
}
export interface ApplicationState {
 nodes: TreeNodeState[]
}

J'ai besoin de créer un sélecteur qui denormalizes l'état ci-dessus pour retourner un graphe d'objets de mise en œuvre de l'interface suivante:

export interface TreeNode {
 id: string;
 text: string;
 children: TreeNode[]
}

C'est, j'ai besoin d'une fonction qui prend un Observables<ApplicationState> et renvoie un Observables<TreeNode[]> tel que chaque TreeNode instance de maintient de l'intégrité référentielle, à moins que l'un de ses enfants a changé.

Idéalement, j'aimerais avoir toute une partie du graphe seulement de mettre à jour ses enfants si ils ont changé la plutôt que de revenir un tout nouveau graphique lorsque toutes les modifications du noeud. Personne ne sait comment un tel sélecteur pourraient être construits à l'aide de ngrx/store et rxjs?

Pour plus d'exemples concrets de ce genre de choses j'ai tenté découvrez l'extrait de code ci-dessous:

// This is the implementation I'm currently using. 
// It works but causes the entire tree to be rerendered
// when any part of the tree changes.
export function getSearchResults(searchText: string = '') {
    return (state$: Observable<ExplorerState>) =>
        Observable.combineLatest(
            state$.let(getFolder(undefined)),
            state$.let(getFolderEntities()),
            state$.let(getDialogEntities()),
            (root, folders, dialogs) =>
                searchFolder(
                    root,
                    id => folders ? folders.get(id) : null,
                    id => folders ? folders.filter(f => f.parentId === id).toArray() : null,
                    id => dialogs ? dialogs.filter(d => d.folderId === id).toArray() : null,
                    searchText
                )
        );
}

function searchFolder(
    folder: FolderState,
    getFolder: (id: string) => FolderState,
    getSubFolders: (id: string) => FolderState[],
    getSubDialogs: (id: string) => DialogSummary[],
    searchText: string
): FolderTree {
  console.log('searching folder', folder ? folder.toJS() : folder);
  const {id, name } = folder;
  const isMatch = (text: string) => !!text && text.toLowerCase().indexOf(searchText) > -1;
  return {
    id,
    name,
    subFolders: getSubFolders(folder.id)
        .map(subFolder => searchFolder(
            subFolder,
            getFolder,
            getSubFolders,
            getSubDialogs,
            searchText))
      .filter(subFolder => subFolder && (!!subFolder.dialogs.length || isMatch(subFolder.name))),
    dialogs: getSubDialogs(id)
      .filter(dialog => dialog && (isMatch(folder.name) || isMatch(dialog.name)))

  } as FolderTree;
}

// This is an alternate implementation using recursion that I'd hoped would do what I wanted
// but is flawed somehow and just never returns a value.
export function getSearchResults2(searchText: string = '', folderId = null)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
    console.debug('Searching folder tree', { searchText, folderId });
    const isMatch = (text: string) =>
        !!text && text.search(new RegExp(searchText, 'i')) >= 0;
    return (state$: Observable<ExplorerState>) =>
        Observable.combineLatest(
            state$.let(getFolder(folderId)),
            state$.let(getContainedFolders(folderId))
                .flatMap(subFolders => subFolders.map(sf => sf.id))
                .flatMap(id => state$.let(getSearchResults2(searchText, id)))
                .toArray(),
            state$.let(getContainedDialogs(folderId)),
            (folder: FolderState, folders: FolderTree[], dialogs: DialogSummary[]) => {
                console.debug('Search complete. constructing tree...', {
                    id: folder.id,
                    name: folder.name,
                    subFolders: folders,
                    dialogs
                });
                return Object.assign({}, {
                    id: folder.id,
                    name: folder.name,
                    subFolders: folders
                        .filter(subFolder =>
                            subFolder.dialogs.length > 0 || isMatch(subFolder.name))
                        .sort((a, b) => a.name.localeCompare(b.name)),
                    dialogs: dialogs
                        .map(dialog => dialog as DialogSummary)
                        .filter(dialog =>
                            isMatch(folder.name)
                            || isMatch(dialog.name))
                        .sort((a, b) => a.name.localeCompare(b.name))
                }) as FolderTree;
            }
        );
}

// This is a similar implementation to the one (uses recursion) above but it is also flawed.
export function getFolderTree(folderId: string)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
    return (state$: Observable<ExplorerState>) => state$
        .let(getFolder(folderId))
        .concatMap(folder =>
            Observable.combineLatest(
                state$.let(getContainedFolders(folderId))
                    .flatMap(subFolders => subFolders.map(sf => sf.id))
                    .concatMap(id => state$.let(getFolderTree(id)))
                    .toArray(),
                state$.let(getContainedDialogs(folderId)),
                (folders: FolderTree[], dialogs: DialogSummary[]) => Object.assign({}, {
                    id: folder.id,
                    name: folder.name,
                    subFolders: folders.sort((a, b) => a.name.localeCompare(b.name)),
                    dialogs: dialogs.map(dialog => dialog as DialogSummary)
                        .sort((a, b) => a.name.localeCompare(b.name))
                }) as FolderTree
            ));
}

2voto

corolla Points 3174

Si disposé à repenser le problème, vous pouvez utiliser Rxjs opérateur de numérisation:

  1. Si aucun précédent ApplicationState existe, accepter la première. Traduire à TreeNodes de manière récursive. Comme c'est un objet réel, rxjs n'est pas impliqué.
  2. Chaque fois qu'un nouvel état de l'application est reçu, c'est à dire lors de l'analyse des incendies, de mettre en œuvre une fonction qui transforme la précédente nœuds à l'aide de l'etat a reçu, et retourne le précédent nœuds dans le contrôle de l'opérateur. Cela vous garantira l'intégrité référentielle.
  3. Vous pouvez être laissé avec un nouveau problème, que des modifications de la muté nœuds de l'arborescence peut pas être ramassé. Si donc, regarder en piste en en faisant une signature pour chaque nœud, ou envisager d'ajouter un changeDetectorRef à un nœud (fournie par le composant de rendu nœud), vous permettant de marquer un composant de mise à jour. Ce sera probablement fournir de meilleurs résultats, que vous pouvez aller avec détection de changement de stratégie OnPush.

Pseudo-code:

state$.scan((state, nodes) => nodes ? mutateNodesBy(nodes, state) : stateToNodes(state))

La sortie est garanti pour préserver l'intégrité référentielle (si possible) que les noeuds sont une fois construit, puis seulement muté.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X