diff --git a/packages/client/src/listeners.js b/packages/client/src/listeners.js index 5131782ab..2f27421c7 100644 --- a/packages/client/src/listeners.js +++ b/packages/client/src/listeners.js @@ -2,8 +2,10 @@ import _ from 'lodash/fp.js' import { eventEmitter, hasSome } from './util/futil.js' import { encode } from './util/tree.js' +let matchesKeys = (keys, delta) => _.isEmpty(keys) || hasSome(keys, delta) + export let setupListeners = (tree) => { - let { on, emit } = eventEmitter() + let { on, onAny, emit } = eventEmitter() // Assume first arg is node which might have path tree.onChange = (node = {}, delta) => emit(encode(node.path), node, delta) // Public API @@ -12,4 +14,14 @@ export let setupListeners = (tree) => { // Trigger watcher if keys match or no keys passed if (_.isEmpty(keys) || hasSome(keys, delta)) f(node, delta) }) + tree.watchTree = (f, keys, path) => + onAny((eventPath, node, delta) => { + // already encoded, so isParent not needed + // Use case is, for example, watching a group and all its children + // might be better solved by watchable group flags + let matchesPath = _.isEmpty(path) || _.startsWith(path, eventPath) + // TODO: should getNode return root for [] or empty paths? + let treeNode = path ? tree.getNode(path) : tree.tree + if (matchesPath && matchesKeys(keys, delta)) f(treeNode, node, delta) + }) } diff --git a/packages/client/src/listeners.test.js b/packages/client/src/listeners.test.js index 3307e23b8..c3c566558 100644 --- a/packages/client/src/listeners.test.js +++ b/packages/client/src/listeners.test.js @@ -192,6 +192,51 @@ let AllTests = (ContextureClient) => { await tree.mutate(['root', 'results'], { pageSize: 2 }) expect(resultWatcher).toBeCalledTimes(2) }) + it('watchTree', async () => { + let service = jest.fn(mockService({ delay: 10 })) + let tree = ContextureClient( + { service, debounce: 1 }, + { + key: 'root', + join: 'and', + children: [ + { + key: 'filter', + type: 'facet', + field: 'facetfield', + values: ['some value'], + }, + { key: 'results', type: 'results' }, + ], + } + ) + let filterDom = '' + let resultsDom = '' + tree.watchTree((root) => { + filterDom = `
+

Facet

+ Field: ${root.children[0].field} + values: ${_.join(', ', root.children[0].values)} +

` + resultsDom = `${_.map( + (result) => + `\n${_.map((val) => ``, _.values(result))}`, + root.children[1].context.results + )} +
${val}
` + }) + expect(filterDom).toBe('') + let action = tree.mutate(['root', 'filter'], { values: ['other Value'] }) + expect(filterDom).toBe(`
+

Facet

+ Field: facetfield + values: other Value +

`) + await action + expect(resultsDom).toBe(` + +
some result
`) + }) }) } diff --git a/packages/client/src/util/futil.js b/packages/client/src/util/futil.js index 8f8e06088..705067ccb 100644 --- a/packages/client/src/util/futil.js +++ b/packages/client/src/util/futil.js @@ -7,18 +7,24 @@ export let hasSome = (keys, obj) => _.some(F.hasIn(obj), keys) // Sets up basic event emitter/listener registry with an array of listeners per topic // e.g. listeners: { topic1: [fn1, fn2, ...], topic2: [...], ... } -export let eventEmitter = (listeners = {}) => ({ - listeners, - emit: (topic, ...args) => _.over(listeners[topic])(...args), - on(topic, fn) { +// Also emit on a special symbol for all topics +let allTopics = Symbol('allTopics') +export let eventEmitter = (listeners = {}) => { + let emit = (topic, ...args) => { + _.over(listeners[topic])(...args) + _.over(listeners[allTopics])(topic, ...args) + } + let on = (topic, fn) => { if (!listeners[topic]) listeners[topic] = [] listeners[topic].push(fn) // unlisten return () => { listeners[topic] = _.without(fn, listeners[topic]) } - }, -}) + } + let onAny = (fn) => on(allTopics, fn) + return { listeners, emit, on, onAny } +} export let transformTreePostOrder = (next = F.traverse) => _.curry((f, x) => {