admin管理员组文章数量:1432602
I've been trying to figure out a way to use the select operator in bination with rxjs's other operators to query a tree data structure (normalized in the store to a flat list) in such a way that it preserves referential integrity for ChangeDetectionStrategy.OnPush semantics but my best attempts cause the entire tree to be rerendered when any part of the tree changes. Does anyone have any ideas? If you consider the following interface as representative of the data in the store:
export interface TreeNodeState {
id: string;
text: string;
children: string[] // the ids of the child nodes
}
export interface ApplicationState {
nodes: TreeNodeState[]
}
I've been trying to figure out a way to use the select operator in bination with rxjs's other operators to query a tree data structure (normalized in the store to a flat list) in such a way that it preserves referential integrity for ChangeDetectionStrategy.OnPush semantics but my best attempts cause the entire tree to be rerendered when any part of the tree changes. Does anyone have any ideas? If you consider the following interface as representative of the data in the store:
export interface TreeNodeState {
id: string;
text: string;
children: string[] // the ids of the child nodes
}
export interface ApplicationState {
nodes: TreeNodeState[]
}
I need to create a selector that denormalizes the state above to return a graph of objects implementing the following interface:
export interface TreeNode {
id: string;
text: string;
children: TreeNode[]
}
That is, I need a function that takes an Observable<ApplicationState> and returns an Observable<TreeNode[]> such that each TreeNode instance maintains referential integrity unless one of its children has changed.
Ideally I'd like to have any one part of the graph only update its children if they've changed rather than return an entirely new graph when any node changes. Does anyone know how such a selector could be constructed using ngrx/store and rxjs?
For more concrete examples of the kinds of things I've attempted check out the snippet below:
// 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.bineLatest(
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.bineLatest(
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 plete. 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.bineLatest(
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
));
}
Share
Improve this question
edited Aug 23, 2016 at 10:34
Jimit
asked Aug 23, 2016 at 10:29
JimitJimit
7652 gold badges10 silver badges18 bronze badges
4
- Have you had any luck implementing this? I'm having the same requirement in my app. – Alexander Ciesielski Commented Sep 22, 2016 at 10:04
-
Can we make the assumption that
ApplicationState.nodes
has parent nodes before that parent's children nodes? – 0xcaff Commented Dec 30, 2016 at 19:00 -
Also, with
OnPush
changes are only propagated after the reference to a property is changed (ormarkForCheck()
is called, but that updates the entire ponent). This means you would have to update the reference to the array, rebuilding causing the entire tree to be checked. Instead of OnPush, you'd probably want to use Immutable.js but I'm not sure how exactly that works with angular. – 0xcaff Commented Dec 30, 2016 at 20:00 - What if you made each node an observable with each parent the observer? – Baron Commented Jun 18, 2017 at 17:56
1 Answer
Reset to default 2If willing to rethink the problem, you could use Rxjs operator scan:
- If no previous ApplicationState exists, accept the first one. Translate it to TreeNodes recursively. As this is an actual object, rxjs isn't involved.
- Whenever a new application state is received, ie when scan fires, implement a function that mutates the previous nodes using the state received, and returns the previous nodes in the scan operator. This will guarantee you referential integrity.
- You might now be left with a new problem, as changes to mutated tree nodes might not be picked up. If so, either look into track by by making a signature for each node, or consider adding a changeDetectorRef to a node (provided by ponent rendering node), allowing you to mark a ponent for update. This will likely perform better, as you can go with change detection strategy OnPush.
Pseudocode:
state$.scan((state, nodes) => nodes ? mutateNodesBy(nodes, state) : stateToNodes(state))
The output is guaranteed to preserve referential integrity (where possible) as nodes are built once, then only mutated.
本文标签: javascriptAngular 2ngrxstoreRxJS and treelike dataStack Overflow
版权声明:本文标题:javascript - Angular 2, ngrxstore, RxJS and tree-like data - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1745606717a2665878.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论