Refactor watch sources for reliability

This commit changes `WatchSource` signatures into `Readonly<Ref>`s.

It provides two important benefits:

1. Eliminates the possibility of `undefined` states, that's result of
   using `WatchSource`s. This previously required additional null checks.
   By using `Readonly<Ref>`, the state handling becomes simpler and less
   susceptible to null errors.
2. Optimizes performance by using references:
   - Avoids the reactive layer of `computed` references when not needed.
   - The `watch` syntax, such as `watch(() => ref.value)`, can introduce
     side effects. For example, it does not account for `triggerRef` in
     scenarios where the value remains unchanged, preventing the watcher
     from running (vuejs/core#9579).
This commit is contained in:
undergroundwires
2023-11-11 13:55:21 +01:00
parent 58cd551a30
commit 7ab16ecccb
25 changed files with 190 additions and 217 deletions

View File

@@ -17,7 +17,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, toRef } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import TreeView from './TreeView/TreeView.vue'; import TreeView from './TreeView/TreeView.vue';
import NodeContent from './NodeContent/NodeContent.vue'; import NodeContent from './NodeContent/NodeContent.vue';
@@ -42,7 +42,7 @@ export default defineComponent({
const useUserCollectionStateHook = injectKey((keys) => keys.useUserSelectionState); const useUserCollectionStateHook = injectKey((keys) => keys.useUserSelectionState);
const { selectedScriptNodeIds } = useSelectedScriptNodeIds(useUserCollectionStateHook); const { selectedScriptNodeIds } = useSelectedScriptNodeIds(useUserCollectionStateHook);
const { latestFilterEvent } = useTreeViewFilterEvent(); const { latestFilterEvent } = useTreeViewFilterEvent();
const { treeViewInputNodes } = useTreeViewNodeInput(() => props.categoryId); const { treeViewInputNodes } = useTreeViewNodeInput(toRef(props, 'categoryId'));
const { updateNodeSelection } = useCollectionSelectionStateUpdater(useUserCollectionStateHook); const { updateNodeSelection } = useCollectionSelectionStateUpdater(useUserCollectionStateHook);
function handleNodeChangedEvent(event: TreeNodeStateChangedEmittedEvent) { function handleNodeChangedEvent(event: TreeNodeStateChangedEmittedEvent) {

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="wrapper" v-if="currentNode"> <div class="wrapper">
<div <div
class="expansible-node" class="expansible-node"
@click="toggleCheck" @click="toggleCheck"
@@ -46,15 +46,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { import { defineComponent, computed, toRef } from 'vue';
defineComponent, computed, PropType,
} from 'vue';
import { TreeRoot } from '../TreeRoot/TreeRoot'; import { TreeRoot } from '../TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes'; import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy'; import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import { useNodeState } from './UseNodeState'; import { useNodeState } from './UseNodeState';
import { TreeNode } from './TreeNode'; import { TreeNode } from './TreeNode';
import LeafTreeNode from './LeafTreeNode.vue'; import LeafTreeNode from './LeafTreeNode.vue';
import type { PropType } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'HierarchicalTreeNode', // Needed due to recursion name: 'HierarchicalTreeNode', // Needed due to recursion
@@ -76,33 +75,32 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
const { nodes } = useCurrentTreeNodes(() => props.treeRoot); const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
const currentNode = computed<TreeNode | undefined>( const currentNode = computed<TreeNode>(
() => nodes.value?.getNodeById(props.nodeId), () => nodes.value.getNodeById(props.nodeId),
); );
const { state } = useNodeState(() => currentNode.value); const { state } = useNodeState(currentNode);
const expanded = computed<boolean>(() => state.value?.isExpanded ?? false); const expanded = computed<boolean>(() => state.value.isExpanded);
const renderedNodeIds = computed<readonly string[]>( const renderedNodeIds = computed<readonly string[]>(
() => currentNode.value () => currentNode.value
?.hierarchy .hierarchy
.children .children
.filter((child) => props.renderingStrategy.shouldRender(child)) .filter((child) => props.renderingStrategy.shouldRender(child))
.map((child) => child.id) .map((child) => child.id),
?? [],
); );
function toggleExpand() { function toggleExpand() {
currentNode.value?.state.toggleExpand(); currentNode.value.state.toggleExpand();
} }
function toggleCheck() { function toggleCheck() {
currentNode.value?.state.toggleCheck(); currentNode.value.state.toggleCheck();
} }
const hasChildren = computed<boolean>( const hasChildren = computed<boolean>(
() => currentNode.value?.hierarchy.isBranchNode, () => currentNode.value.hierarchy.isBranchNode,
); );
return { return {

View File

@@ -1,6 +1,5 @@
<template> <template>
<li <li
v-if="currentNode"
class="wrapper" class="wrapper"
@click.stop="toggleCheckState" @click.stop="toggleCheckState"
> >
@@ -31,15 +30,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { import { defineComponent, computed, toRef } from 'vue';
defineComponent, computed, PropType,
} from 'vue';
import { TreeRoot } from '../TreeRoot/TreeRoot'; import { TreeRoot } from '../TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes'; import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { useNodeState } from './UseNodeState'; import { useNodeState } from './UseNodeState';
import { useKeyboardInteractionState } from './UseKeyboardInteractionState'; import { useKeyboardInteractionState } from './UseKeyboardInteractionState';
import { TreeNode } from './TreeNode'; import { TreeNode } from './TreeNode';
import { TreeNodeCheckState } from './State/CheckState'; import { TreeNodeCheckState } from './State/CheckState';
import type { PropType } from 'vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -54,16 +52,16 @@ export default defineComponent({
}, },
setup(props) { setup(props) {
const { isKeyboardBeingUsed } = useKeyboardInteractionState(); const { isKeyboardBeingUsed } = useKeyboardInteractionState();
const { nodes } = useCurrentTreeNodes(() => props.treeRoot); const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
const currentNode = computed<TreeNode | undefined>( const currentNode = computed<TreeNode>(
() => nodes.value?.getNodeById(props.nodeId), () => nodes.value.getNodeById(props.nodeId),
); );
const { state } = useNodeState(() => currentNode.value); const { state } = useNodeState(currentNode);
const hasFocus = computed<boolean>(() => state.value?.isFocused ?? false); const hasFocus = computed<boolean>(() => state.value.isFocused);
const checked = computed<boolean>(() => state.value?.checkState === TreeNodeCheckState.Checked); const checked = computed<boolean>(() => state.value.checkState === TreeNodeCheckState.Checked);
const indeterminate = computed<boolean>( const indeterminate = computed<boolean>(
() => state.value?.checkState === TreeNodeCheckState.Indeterminate, () => state.value.checkState === TreeNodeCheckState.Indeterminate,
); );
const hasKeyboardFocus = computed<boolean>(() => { const hasKeyboardFocus = computed<boolean>(() => {
@@ -74,14 +72,11 @@ export default defineComponent({
}); });
const onNodeFocus = () => { const onNodeFocus = () => {
if (!currentNode.value) {
return;
}
props.treeRoot.focus.setSingleFocus(currentNode.value); props.treeRoot.focus.setSingleFocus(currentNode.value);
}; };
function toggleCheckState() { function toggleCheckState() {
currentNode.value?.state.toggleCheck(); currentNode.value.state.toggleCheck();
} }
return { return {

View File

@@ -1,21 +1,18 @@
import { import {
WatchSource, shallowRef, watch, type Ref, shallowRef, watch, shallowReadonly,
} from 'vue'; } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import { ReadOnlyTreeNode } from './TreeNode'; import { ReadOnlyTreeNode } from './TreeNode';
import { TreeNodeStateDescriptor } from './State/StateDescriptor'; import { TreeNodeStateDescriptor } from './State/StateDescriptor';
export function useNodeState( export function useNodeState(
nodeWatcher: WatchSource<ReadOnlyTreeNode | undefined>, nodeRef: Readonly<Ref<ReadOnlyTreeNode>>,
) { ) {
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents); const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
const state = shallowRef<TreeNodeStateDescriptor>(); const state = shallowRef<TreeNodeStateDescriptor>(nodeRef.value.state.current);
watch(nodeWatcher, (node: ReadOnlyTreeNode) => { watch(nodeRef, (node: ReadOnlyTreeNode) => {
if (!node) {
return;
}
state.value = node.state.current; state.value = node.state.current;
events.unsubscribeAllAndRegister([ events.unsubscribeAllAndRegister([
node.state.changed.on((change) => { node.state.changed.on((change) => {
@@ -25,6 +22,6 @@ export function useNodeState(
}, { immediate: true }); }, { immediate: true });
return { return {
state, state: shallowReadonly(state),
}; };
} }

View File

@@ -1,5 +1,5 @@
import { import {
WatchSource, shallowRef, triggerRef, watch, type Ref, shallowRef, triggerRef, watch,
} from 'vue'; } from 'vue';
import { ReadOnlyTreeNode } from '../Node/TreeNode'; import { ReadOnlyTreeNode } from '../Node/TreeNode';
import { useNodeStateChangeAggregator } from '../UseNodeStateChangeAggregator'; import { useNodeStateChangeAggregator } from '../UseNodeStateChangeAggregator';
@@ -15,7 +15,7 @@ import { CollapsedParentOrderer } from './Ordering/CollapsedParentOrderer';
* Renders tree nodes gradually to prevent UI freeze when loading large amounts of nodes. * Renders tree nodes gradually to prevent UI freeze when loading large amounts of nodes.
*/ */
export function useGradualNodeRendering( export function useGradualNodeRendering(
treeWatcher: WatchSource<TreeRoot>, treeRootRef: Readonly<Ref<TreeRoot>>,
useChangeAggregator = useNodeStateChangeAggregator, useChangeAggregator = useNodeStateChangeAggregator,
useTreeNodes = useCurrentTreeNodes, useTreeNodes = useCurrentTreeNodes,
scheduler: DelayScheduler = new TimeoutDelayScheduler(), scheduler: DelayScheduler = new TimeoutDelayScheduler(),
@@ -28,8 +28,8 @@ export function useGradualNodeRendering(
let isRenderingInProgress = false; let isRenderingInProgress = false;
const renderingDelayInMs = 50; const renderingDelayInMs = 50;
const { onNodeStateChange } = useChangeAggregator(treeWatcher); const { onNodeStateChange } = useChangeAggregator(treeRootRef);
const { nodes } = useTreeNodes(treeWatcher); const { nodes } = useTreeNodes(treeRootRef);
function updateNodeRenderQueue(node: ReadOnlyTreeNode, isVisible: boolean) { function updateNodeRenderQueue(node: ReadOnlyTreeNode, isVisible: boolean) {
if (isVisible if (isVisible
@@ -48,7 +48,7 @@ export function useGradualNodeRendering(
} }
} }
watch(() => nodes.value, (newNodes) => { watch(nodes, (newNodes) => {
nodesToRender.clear(); nodesToRender.clear();
nodesBeingRendered.value.clear(); nodesBeingRendered.value.clear();
if (!newNodes || newNodes.flattenedNodes.length === 0) { if (!newNodes || newNodes.flattenedNodes.length === 0) {

View File

@@ -18,12 +18,13 @@
<script lang="ts"> <script lang="ts">
import { import {
defineComponent, computed, PropType, defineComponent, computed, toRef,
} from 'vue'; } from 'vue';
import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue'; import HierarchicalTreeNode from '../Node/HierarchicalTreeNode.vue';
import { useCurrentTreeNodes } from '../UseCurrentTreeNodes'; import { useCurrentTreeNodes } from '../UseCurrentTreeNodes';
import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy'; import { NodeRenderingStrategy } from '../Rendering/Scheduling/NodeRenderingStrategy';
import { TreeRoot } from './TreeRoot'; import { TreeRoot } from './TreeRoot';
import type { PropType } from 'vue';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -40,7 +41,7 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
const { nodes } = useCurrentTreeNodes(() => props.treeRoot); const { nodes } = useCurrentTreeNodes(toRef(props, 'treeRoot'));
const renderedNodeIds = computed<string[]>(() => { const renderedNodeIds = computed<string[]>(() => {
return nodes return nodes

View File

@@ -14,7 +14,7 @@
<script lang="ts"> <script lang="ts">
import { import {
defineComponent, onMounted, watch, defineComponent, onMounted, watch,
shallowRef, PropType, shallowRef, toRef, shallowReadonly,
} from 'vue'; } from 'vue';
import { TreeRootManager } from './TreeRoot/TreeRootManager'; import { TreeRootManager } from './TreeRoot/TreeRootManager';
import TreeRoot from './TreeRoot/TreeRoot.vue'; import TreeRoot from './TreeRoot/TreeRoot.vue';
@@ -28,6 +28,7 @@ import { TreeNodeStateChangedEmittedEvent } from './Bindings/TreeNodeStateChange
import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState'; import { useAutoUpdateParentCheckState } from './UseAutoUpdateParentCheckState';
import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState'; import { useAutoUpdateChildrenCheckState } from './UseAutoUpdateChildrenCheckState';
import { useGradualNodeRendering } from './Rendering/UseGradualNodeRendering'; import { useGradualNodeRendering } from './Rendering/UseGradualNodeRendering';
import type { PropType } from 'vue';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -57,17 +58,16 @@ export default defineComponent({
const tree = new TreeRootManager(); const tree = new TreeRootManager();
useTreeKeyboardNavigation(tree, treeContainerElement); const treeRef = shallowReadonly(shallowRef(tree));
useTreeQueryFilter(
() => props.latestFilterEvent,
() => tree,
);
useLeafNodeCheckedStateUpdater(() => tree, () => props.selectedLeafNodeIds);
useAutoUpdateParentCheckState(() => tree);
useAutoUpdateChildrenCheckState(() => tree);
const nodeRenderingScheduler = useGradualNodeRendering(() => tree);
const { onNodeStateChange } = useNodeStateChangeAggregator(() => tree); useTreeKeyboardNavigation(treeRef, treeContainerElement);
useTreeQueryFilter(toRef(props, 'latestFilterEvent'), treeRef);
useLeafNodeCheckedStateUpdater(treeRef, toRef(props, 'selectedLeafNodeIds'));
useAutoUpdateParentCheckState(treeRef);
useAutoUpdateChildrenCheckState(treeRef);
const nodeRenderingScheduler = useGradualNodeRendering(treeRef);
const { onNodeStateChange } = useNodeStateChangeAggregator(treeRef);
onNodeStateChange((change) => { onNodeStateChange((change) => {
emit('nodeStateChanged', { emit('nodeStateChanged', {

View File

@@ -1,14 +1,14 @@
import { WatchSource } from 'vue';
import { TreeRoot } from './TreeRoot/TreeRoot'; import { TreeRoot } from './TreeRoot/TreeRoot';
import { useNodeStateChangeAggregator } from './UseNodeStateChangeAggregator'; import { useNodeStateChangeAggregator } from './UseNodeStateChangeAggregator';
import { HierarchyAccess } from './Node/Hierarchy/HierarchyAccess'; import { HierarchyAccess } from './Node/Hierarchy/HierarchyAccess';
import { TreeNodeCheckState } from './Node/State/CheckState'; import { TreeNodeCheckState } from './Node/State/CheckState';
import type { Ref } from 'vue';
export function useAutoUpdateChildrenCheckState( export function useAutoUpdateChildrenCheckState(
treeWatcher: WatchSource<TreeRoot>, treeRootRef: Readonly<Ref<TreeRoot>>,
useChangeAggregator = useNodeStateChangeAggregator, useChangeAggregator = useNodeStateChangeAggregator,
) { ) {
const { onNodeStateChange } = useChangeAggregator(treeWatcher); const { onNodeStateChange } = useChangeAggregator(treeRootRef);
onNodeStateChange((change) => { onNodeStateChange((change) => {
if (change.newState.checkState === change.oldState?.checkState) { if (change.newState.checkState === change.oldState?.checkState) {

View File

@@ -1,15 +1,15 @@
import { WatchSource } from 'vue';
import { TreeRoot } from './TreeRoot/TreeRoot'; import { TreeRoot } from './TreeRoot/TreeRoot';
import { useNodeStateChangeAggregator } from './UseNodeStateChangeAggregator'; import { useNodeStateChangeAggregator } from './UseNodeStateChangeAggregator';
import { HierarchyAccess } from './Node/Hierarchy/HierarchyAccess'; import { HierarchyAccess } from './Node/Hierarchy/HierarchyAccess';
import { TreeNodeCheckState } from './Node/State/CheckState'; import { TreeNodeCheckState } from './Node/State/CheckState';
import { ReadOnlyTreeNode } from './Node/TreeNode'; import { ReadOnlyTreeNode } from './Node/TreeNode';
import type { Ref } from 'vue';
export function useAutoUpdateParentCheckState( export function useAutoUpdateParentCheckState(
treeWatcher: WatchSource<TreeRoot>, treeRef: Readonly<Ref<TreeRoot>>,
useChangeAggregator = useNodeStateChangeAggregator, useChangeAggregator = useNodeStateChangeAggregator,
) { ) {
const { onNodeStateChange } = useChangeAggregator(treeWatcher); const { onNodeStateChange } = useChangeAggregator(treeRef);
onNodeStateChange((change) => { onNodeStateChange((change) => {
if (change.newState.checkState === change.oldState?.checkState) { if (change.newState.checkState === change.oldState?.checkState) {

View File

@@ -1,18 +1,16 @@
import { import {
WatchSource, watch, shallowReadonly, shallowRef, watch, shallowReadonly, shallowRef, type Ref,
} from 'vue'; } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import { TreeRoot } from './TreeRoot/TreeRoot'; import { TreeRoot } from './TreeRoot/TreeRoot';
import { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes'; import { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes';
export function useCurrentTreeNodes(treeWatcher: WatchSource<TreeRoot>) { export function useCurrentTreeNodes(treeRef: Readonly<Ref<TreeRoot>>) {
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents); const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
const tree = shallowRef<TreeRoot | undefined>(); const nodes = shallowRef<QueryableNodes>(treeRef.value.collection.nodes);
const nodes = shallowRef<QueryableNodes | undefined>();
watch(treeWatcher, (newTree) => { watch(treeRef, (newTree) => {
tree.value = newTree;
nodes.value = newTree.collection.nodes; nodes.value = newTree.collection.nodes;
events.unsubscribeAllAndRegister([ events.unsubscribeAllAndRegister([
newTree.collection.nodesUpdated.on((newNodes) => { newTree.collection.nodesUpdated.on((newNodes) => {

View File

@@ -1,4 +1,4 @@
import { WatchSource, watch } from 'vue'; import { type Ref, watch } from 'vue';
import { TreeRoot } from './TreeRoot/TreeRoot'; import { TreeRoot } from './TreeRoot/TreeRoot';
import { TreeNode } from './Node/TreeNode'; import { TreeNode } from './Node/TreeNode';
import { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes'; import { QueryableNodes } from './TreeRoot/NodeCollection/Query/QueryableNodes';
@@ -6,13 +6,13 @@ import { useCurrentTreeNodes } from './UseCurrentTreeNodes';
import { TreeNodeCheckState } from './Node/State/CheckState'; import { TreeNodeCheckState } from './Node/State/CheckState';
export function useLeafNodeCheckedStateUpdater( export function useLeafNodeCheckedStateUpdater(
treeWatcher: WatchSource<TreeRoot>, treeRootRef: Readonly<Ref<TreeRoot>>,
leafNodeIdsWatcher: WatchSource<readonly string[]>, leafNodeIdsRef: Readonly<Ref<readonly string[]>>,
) { ) {
const { nodes } = useCurrentTreeNodes(treeWatcher); const { nodes } = useCurrentTreeNodes(treeRootRef);
watch( watch(
[leafNodeIdsWatcher, () => nodes.value], [leafNodeIdsRef, nodes],
([nodeIds, actualNodes]) => { ([nodeIds, actualNodes]) => {
updateNodeSelections(actualNodes, nodeIds); updateNodeSelections(actualNodes, nodeIds);
}, },

View File

@@ -1,5 +1,5 @@
import { import {
WatchSource, watch, shallowRef, watch, shallowRef, type Ref,
} from 'vue'; } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
import { IEventSubscription } from '@/infrastructure/Events/IEventSource'; import { IEventSubscription } from '@/infrastructure/Events/IEventSource';
@@ -11,31 +11,31 @@ import { TreeNodeStateDescriptor } from './Node/State/StateDescriptor';
export type NodeStateChangeEventCallback = (args: NodeStateChangeEventArgs) => void; export type NodeStateChangeEventCallback = (args: NodeStateChangeEventArgs) => void;
export function useNodeStateChangeAggregator( export function useNodeStateChangeAggregator(
treeWatcher: WatchSource<TreeRoot>, treeRootRef: Readonly<Ref<TreeRoot>>,
useTreeNodes = useCurrentTreeNodes, useTreeNodes = useCurrentTreeNodes,
) { ) {
const { nodes } = useTreeNodes(treeWatcher); const { nodes } = useTreeNodes(treeRootRef);
const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents); const { events } = injectKey((keys) => keys.useAutoUnsubscribedEvents);
const onNodeChangeCallback = shallowRef<NodeStateChangeEventCallback>(); const onNodeChangeCallback = shallowRef<NodeStateChangeEventCallback>();
watch([ watch(
() => nodes.value, [nodes, onNodeChangeCallback],
() => onNodeChangeCallback.value, ([newNodes, callback]) => {
], ([newNodes, callback]) => { if (!callback) { // may not be registered yet
if (!callback) { // might not be registered yet return;
return; }
} if (!newNodes || newNodes.flattenedNodes.length === 0) {
if (!newNodes || newNodes.flattenedNodes.length === 0) { events.unsubscribeAll();
events.unsubscribeAll(); return;
return; }
} const allNodes = newNodes.flattenedNodes;
const allNodes = newNodes.flattenedNodes; events.unsubscribeAllAndRegister(
events.unsubscribeAllAndRegister( subscribeToNotifyOnFutureNodeChanges(allNodes, callback),
subscribeToNotifyOnFutureNodeChanges(allNodes, callback), );
); notifyCurrentNodeState(allNodes, callback);
notifyCurrentNodeState(allNodes, callback); },
}); );
function onNodeStateChange( function onNodeStateChange(
callback: NodeStateChangeEventCallback, callback: NodeStateChangeEventCallback,

View File

@@ -8,14 +8,16 @@ import { TreeNode } from './Node/TreeNode';
type TreeNavigationKeyCodes = 'ArrowLeft' | 'ArrowUp' | 'ArrowRight' | 'ArrowDown' | ' ' | 'Enter'; type TreeNavigationKeyCodes = 'ArrowLeft' | 'ArrowUp' | 'ArrowRight' | 'ArrowDown' | ' ' | 'Enter';
export function useTreeKeyboardNavigation( export function useTreeKeyboardNavigation(
treeRoot: TreeRoot, treeRootRef: Readonly<Ref<TreeRoot>>,
treeElementRef: Ref<HTMLElement | undefined>, treeElementRef: Readonly<Ref<HTMLElement | undefined>>,
) { ) {
useKeyboardListener(treeElementRef, (event) => { useKeyboardListener(treeElementRef, (event) => {
if (!treeElementRef.value) { if (!treeElementRef.value) {
return; // Not yet initialized? return; // Not yet initialized?
} }
const treeRoot = treeRootRef.value;
const keyCode = event.key as TreeNavigationKeyCodes; const keyCode = event.key as TreeNavigationKeyCodes;
if (!treeRoot.focus.currentSingleFocusedNode) { if (!treeRoot.focus.currentSingleFocusedNode) {
@@ -39,7 +41,7 @@ export function useTreeKeyboardNavigation(
} }
function useKeyboardListener( function useKeyboardListener(
elementRef: Ref<HTMLElement | undefined>, elementRef: Readonly<Ref<HTMLElement | undefined>>,
handleKeyboardEvent: (event: KeyboardEvent) => void, handleKeyboardEvent: (event: KeyboardEvent) => void,
) { ) {
onMounted(() => { onMounted(() => {

View File

@@ -1,4 +1,4 @@
import { WatchSource, watch } from 'vue'; import { type Ref, watch } from 'vue';
import { TreeViewFilterAction, TreeViewFilterEvent, TreeViewFilterPredicate } from './Bindings/TreeInputFilterEvent'; import { TreeViewFilterAction, TreeViewFilterEvent, TreeViewFilterPredicate } from './Bindings/TreeInputFilterEvent';
import { ReadOnlyTreeNode, TreeNode } from './Node/TreeNode'; import { ReadOnlyTreeNode, TreeNode } from './Node/TreeNode';
import { TreeRoot } from './TreeRoot/TreeRoot'; import { TreeRoot } from './TreeRoot/TreeRoot';
@@ -8,18 +8,18 @@ import { TreeNodeStateTransaction } from './Node/State/StateAccess';
import { TreeNodeStateDescriptor } from './Node/State/StateDescriptor'; import { TreeNodeStateDescriptor } from './Node/State/StateDescriptor';
export function useTreeQueryFilter( export function useTreeQueryFilter(
latestFilterEventWatcher: WatchSource<TreeViewFilterEvent | undefined>, latestFilterEventRef: Readonly<Ref<TreeViewFilterEvent | undefined>>,
treeWatcher: WatchSource<TreeRoot>, treeRootRef: Readonly<Ref<TreeRoot>>,
) { ) {
const { nodes } = useCurrentTreeNodes(treeWatcher); const { nodes } = useCurrentTreeNodes(treeRootRef);
let isFiltering = false; let isFiltering = false;
const statesBeforeFiltering = new NodeStateRestorer(); const statesBeforeFiltering = new NodeStateRestorer();
statesBeforeFiltering.saveStateBeforeFilter(nodes.value); statesBeforeFiltering.saveStateBeforeFilter(nodes.value);
setupWatchers({ setupWatchers({
filterEventWatcher: latestFilterEventWatcher, filterEventRef: latestFilterEventRef,
nodesWatcher: () => nodes.value, nodesRef: nodes,
onFilterTrigger: (predicate, newNodes) => runFilter( onFilterTrigger: (predicate, newNodes) => runFilter(
newNodes, newNodes,
predicate, predicate,
@@ -171,18 +171,18 @@ class NodeStateRestorer {
} }
function setupWatchers(options: { function setupWatchers(options: {
filterEventWatcher: WatchSource<TreeViewFilterEvent | undefined>, readonly filterEventRef: Readonly<Ref<TreeViewFilterEvent | undefined>>,
nodesWatcher: WatchSource<QueryableNodes>, readonly nodesRef: Readonly<Ref<QueryableNodes>>,
onFilterReset: () => void, readonly onFilterReset: () => void,
onFilterTrigger: ( readonly onFilterTrigger: (
predicate: TreeViewFilterPredicate, predicate: TreeViewFilterPredicate,
nodes: QueryableNodes, nodes: QueryableNodes,
) => void, ) => void,
}) { }) {
watch( watch(
[ [
options.filterEventWatcher, options.filterEventRef,
options.nodesWatcher, options.nodesRef,
], ],
([filterEvent, nodes]) => { ([filterEvent, nodes]) => {
if (!filterEvent) { if (!filterEvent) {

View File

@@ -1,6 +1,5 @@
import { import {
WatchSource, computed, type Ref, computed, shallowReadonly,
ref, watch,
} from 'vue'; } from 'vue';
import { ICategoryCollection } from '@/domain/ICategoryCollection'; import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { injectKey } from '@/presentation/injectionSymbols'; import { injectKey } from '@/presentation/injectionSymbols';
@@ -10,7 +9,7 @@ import { convertToNodeInput } from './TreeNodeMetadataConverter';
import { parseSingleCategory, parseAllCategories } from './CategoryNodeMetadataConverter'; import { parseSingleCategory, parseAllCategories } from './CategoryNodeMetadataConverter';
export function useTreeViewNodeInput( export function useTreeViewNodeInput(
categoryIdWatcher: WatchSource<number | undefined>, categoryIdRef: Readonly<Ref<number | undefined>>,
parser: CategoryNodeParser = { parser: CategoryNodeParser = {
parseSingle: parseSingleCategory, parseSingle: parseSingleCategory,
parseAll: parseAllCategories, parseAll: parseAllCategories,
@@ -19,20 +18,14 @@ export function useTreeViewNodeInput(
) { ) {
const { currentState } = injectKey((keys) => keys.useCollectionState); const { currentState } = injectKey((keys) => keys.useCollectionState);
const categoryId = ref<number | undefined>();
watch(categoryIdWatcher, (newCategoryId) => {
categoryId.value = newCategoryId;
}, { immediate: true });
const nodes = computed<readonly TreeInputNodeData[]>(() => { const nodes = computed<readonly TreeInputNodeData[]>(() => {
const nodeMetadataList = parseNodes(categoryId.value, currentState.value.collection, parser); const nodeMetadataList = parseNodes(categoryIdRef.value, currentState.value.collection, parser);
const nodeInputs = nodeMetadataList.map((node) => nodeConverter(node)); const nodeInputs = nodeMetadataList.map((node) => nodeConverter(node));
return nodeInputs; return nodeInputs;
}); });
return { return {
treeViewInputNodes: nodes, treeViewInputNodes: shallowReadonly(nodes),
}; };
} }

View File

@@ -1,6 +1,9 @@
import { WatchSource, watch } from 'vue'; import { WatchSource, watch } from 'vue';
export function waitForValueChange<T>(valueWatcher: WatchSource<T>, timeoutMs = 2000): Promise<T> { export function waitForValueChange<T>(
valueWatcher: WatchSource<T>,
timeoutMs = 2000,
): Promise<T> {
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
const unwatch = watch(valueWatcher, (newValue, oldValue) => { const unwatch = watch(valueWatcher, (newValue, oldValue) => {
if (newValue !== oldValue) { if (newValue !== oldValue) {

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { import {
shallowRef, defineComponent, WatchSource, nextTick, shallowRef, defineComponent, nextTick, type Ref,
} from 'vue'; } from 'vue';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode'; import { ReadOnlyTreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
@@ -16,31 +16,22 @@ describe('useNodeState', () => {
it('should set state on immediate invocation if node exists', () => { it('should set state on immediate invocation if node exists', () => {
// arrange // arrange
const expectedState = new TreeNodeStateDescriptorStub(); const expectedState = new TreeNodeStateDescriptorStub();
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined); const nodeRef = shallowRef<ReadOnlyTreeNode>(
nodeWatcher.value = new TreeNodeStub() new TreeNodeStub().withState(new TreeNodeStateAccessStub().withCurrent(expectedState)),
.withState(new TreeNodeStateAccessStub().withCurrent(expectedState)); );
// act // act
const { returnObject } = mountWrapperComponent(nodeWatcher); const { returnObject } = mountWrapperComponent(nodeRef);
// assert // assert
expect(returnObject.state.value).to.equal(expectedState); expect(returnObject.state.value).to.equal(expectedState);
}); });
it('should not set state on immediate invocation if node is undefined', () => { it('should update state when node changes', async () => {
// arrange
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
// act
const { returnObject } = mountWrapperComponent(nodeWatcher);
// assert
expect(returnObject.state.value).toBeUndefined();
});
it('should update state when nodeWatcher changes', async () => {
// arrange // arrange
const expectedNewState = new TreeNodeStateDescriptorStub(); const expectedNewState = new TreeNodeStateDescriptorStub();
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined); const nodeRef = shallowRef<ReadOnlyTreeNode>(new TreeNodeStub());
const { returnObject } = mountWrapperComponent(nodeWatcher); const { returnObject } = mountWrapperComponent(nodeRef);
// act // act
nodeWatcher.value = new TreeNodeStub() nodeRef.value = new TreeNodeStub()
.withState(new TreeNodeStateAccessStub().withCurrent(expectedNewState)); .withState(new TreeNodeStateAccessStub().withCurrent(expectedNewState));
await nextTick(); await nextTick();
// assert // assert
@@ -49,30 +40,28 @@ describe('useNodeState', () => {
it('should update state when node state changes', () => { it('should update state when node state changes', () => {
// arrange // arrange
const nodeWatcher = shallowRef<ReadOnlyTreeNode | undefined>(undefined);
const stateAccessStub = new TreeNodeStateAccessStub(); const stateAccessStub = new TreeNodeStateAccessStub();
const nodeRef = shallowRef<ReadOnlyTreeNode>(
new TreeNodeStub().withState(stateAccessStub),
);
const expectedChangedState = new TreeNodeStateDescriptorStub(); const expectedChangedState = new TreeNodeStateDescriptorStub();
nodeWatcher.value = new TreeNodeStub()
.withState(stateAccessStub);
// act // act
const { returnObject } = mountWrapperComponent(nodeWatcher); const { returnObject } = mountWrapperComponent(nodeRef);
stateAccessStub.triggerStateChangedEvent( stateAccessStub.triggerStateChangedEvent(
new NodeStateChangedEventStub() new NodeStateChangedEventStub()
.withNewState(expectedChangedState), .withNewState(expectedChangedState),
); );
// assert // assert
expect(returnObject.state.value).to.equal(expectedChangedState); expect(returnObject.state.value).to.equal(expectedChangedState);
}); });
}); });
function mountWrapperComponent(nodeWatcher: WatchSource<ReadOnlyTreeNode | undefined>) { function mountWrapperComponent(nodeRef: Readonly<Ref<ReadOnlyTreeNode>>) {
let returnObject: ReturnType<typeof useNodeState>; let returnObject: ReturnType<typeof useNodeState>;
const wrapper = shallowMount( const wrapper = shallowMount(
defineComponent({ defineComponent({
setup() { setup() {
returnObject = useNodeState(nodeWatcher); returnObject = useNodeState(nodeRef);
}, },
template: '<div></div>', template: '<div></div>',
}), }),

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { WatchSource } from 'vue'; import { type Ref, shallowRef } from 'vue';
import { useGradualNodeRendering } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering'; import { useGradualNodeRendering } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/UseGradualNodeRendering';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot'; import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub'; import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub';
@@ -17,18 +17,18 @@ import { RenderQueueOrdererStub } from '@tests/unit/shared/Stubs/RenderQueueOrde
import { RenderQueueOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/RenderQueueOrderer'; import { RenderQueueOrderer } from '@/presentation/components/Scripts/View/Tree/TreeView/Rendering/Ordering/RenderQueueOrderer';
describe('useGradualNodeRendering', () => { describe('useGradualNodeRendering', () => {
it('watches nodes on specified tree', () => { it('tracks nodes on specified tree', () => {
// arrange // arrange
const expectedWatcher = () => new TreeRootStub(); const expectedTreeRootRef = shallowRef(new TreeRootStub());
const currentTreeNodesStub = new UseCurrentTreeNodesStub(); const currentTreeNodesStub = new UseCurrentTreeNodesStub();
const builder = new UseGradualNodeRenderingBuilder() const builder = new UseGradualNodeRenderingBuilder()
.withCurrentTreeNodes(currentTreeNodesStub) .withCurrentTreeNodes(currentTreeNodesStub)
.withTreeWatcher(expectedWatcher); .withTreeRootRef(expectedTreeRootRef);
// act // act
builder.call(); builder.call();
// assert // assert
const actualWatcher = currentTreeNodesStub.treeWatcher; const actualTreeRootRef = currentTreeNodesStub.treeRootRef;
expect(actualWatcher).to.equal(expectedWatcher); expect(actualTreeRootRef).to.equal(expectedTreeRootRef);
}); });
describe('shouldRender', () => { describe('shouldRender', () => {
describe('on visibility toggle', () => { describe('on visibility toggle', () => {
@@ -269,7 +269,7 @@ function createNodeWithVisibility(
class UseGradualNodeRenderingBuilder { class UseGradualNodeRenderingBuilder {
private changeAggregator = new UseNodeStateChangeAggregatorStub(); private changeAggregator = new UseNodeStateChangeAggregatorStub();
private treeWatcher: WatchSource<TreeRoot | undefined> = () => new TreeRootStub(); private treeRootRef: Readonly<Ref<TreeRoot>> = shallowRef(new TreeRootStub());
private currentTreeNodes = new UseCurrentTreeNodesStub(); private currentTreeNodes = new UseCurrentTreeNodesStub();
@@ -291,8 +291,8 @@ class UseGradualNodeRenderingBuilder {
return this; return this;
} }
public withTreeWatcher(treeWatcher: WatchSource<TreeRoot | undefined>): this { public withTreeRootRef(treeRootRef: Readonly<Ref<TreeRoot>>): this {
this.treeWatcher = treeWatcher; this.treeRootRef = treeRootRef;
return this; return this;
} }
@@ -318,7 +318,7 @@ class UseGradualNodeRenderingBuilder {
public call(): ReturnType<typeof useGradualNodeRendering> { public call(): ReturnType<typeof useGradualNodeRendering> {
return useGradualNodeRendering( return useGradualNodeRendering(
this.treeWatcher, this.treeRootRef,
this.changeAggregator.get(), this.changeAggregator.get(),
this.currentTreeNodes.get(), this.currentTreeNodes.get(),
this.delayScheduler, this.delayScheduler,

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { WatchSource } from 'vue'; import { type Ref, shallowRef } from 'vue';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot'; import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
import { useAutoUpdateChildrenCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateChildrenCheckState'; import { useAutoUpdateChildrenCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateChildrenCheckState';
import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState'; import { TreeNodeCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/CheckState';
@@ -25,16 +25,16 @@ describe('useAutoUpdateChildrenCheckState', () => {
}); });
it('aggregate changes on specified tree', () => { it('aggregate changes on specified tree', () => {
// arrange // arrange
const expectedWatcher = () => new TreeRootStub(); const expectedTreeRoot = shallowRef(new TreeRootStub());
const aggregatorStub = new UseNodeStateChangeAggregatorStub(); const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const builder = new UseAutoUpdateChildrenCheckStateBuilder() const builder = new UseAutoUpdateChildrenCheckStateBuilder()
.withChangeAggregator(aggregatorStub) .withChangeAggregator(aggregatorStub)
.withTreeWatcher(expectedWatcher); .withTreeRoot(expectedTreeRoot);
// act // act
builder.call(); builder.call();
// assert // assert
const actualWatcher = aggregatorStub.treeWatcher; const actualTreeRoot = aggregatorStub.treeRootRef;
expect(actualWatcher).to.equal(expectedWatcher); expect(actualTreeRoot).to.equal(expectedTreeRoot);
}); });
describe('skips event handling', () => { describe('skips event handling', () => {
const scenarios: ReadonlyArray<{ const scenarios: ReadonlyArray<{
@@ -197,21 +197,21 @@ function getAllPossibleCheckStates() {
class UseAutoUpdateChildrenCheckStateBuilder { class UseAutoUpdateChildrenCheckStateBuilder {
private changeAggregator = new UseNodeStateChangeAggregatorStub(); private changeAggregator = new UseNodeStateChangeAggregatorStub();
private treeWatcher: WatchSource<TreeRoot | undefined> = () => new TreeRootStub(); private treeRoot: Readonly<Ref<TreeRoot>> = shallowRef(new TreeRootStub());
public withChangeAggregator(changeAggregator: UseNodeStateChangeAggregatorStub): this { public withChangeAggregator(changeAggregator: UseNodeStateChangeAggregatorStub): this {
this.changeAggregator = changeAggregator; this.changeAggregator = changeAggregator;
return this; return this;
} }
public withTreeWatcher(treeWatcher: WatchSource<TreeRoot | undefined>): this { public withTreeRoot(treeRoot: Readonly<Ref<TreeRoot>>): this {
this.treeWatcher = treeWatcher; this.treeRoot = treeRoot;
return this; return this;
} }
public call(): ReturnType<typeof useAutoUpdateChildrenCheckState> { public call(): ReturnType<typeof useAutoUpdateChildrenCheckState> {
return useAutoUpdateChildrenCheckState( return useAutoUpdateChildrenCheckState(
this.treeWatcher, this.treeRoot,
this.changeAggregator.get(), this.changeAggregator.get(),
); );
} }

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { WatchSource } from 'vue'; import { type Ref, shallowRef } from 'vue';
import { UseNodeStateChangeAggregatorStub } from '@tests/unit/shared/Stubs/UseNodeStateChangeAggregatorStub'; import { UseNodeStateChangeAggregatorStub } from '@tests/unit/shared/Stubs/UseNodeStateChangeAggregatorStub';
import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub'; import { TreeRootStub } from '@tests/unit/shared/Stubs/TreeRootStub';
import { useAutoUpdateParentCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateParentCheckState'; import { useAutoUpdateParentCheckState } from '@/presentation/components/Scripts/View/Tree/TreeView/UseAutoUpdateParentCheckState';
@@ -24,16 +24,16 @@ describe('useAutoUpdateParentCheckState', () => {
}); });
it('aggregate changes on specified tree', () => { it('aggregate changes on specified tree', () => {
// arrange // arrange
const expectedWatcher = () => new TreeRootStub(); const expectedTreeRootRef = shallowRef(new TreeRootStub());
const aggregatorStub = new UseNodeStateChangeAggregatorStub(); const aggregatorStub = new UseNodeStateChangeAggregatorStub();
const builder = new UseAutoUpdateParentCheckStateBuilder() const builder = new UseAutoUpdateParentCheckStateBuilder()
.withChangeAggregator(aggregatorStub) .withChangeAggregator(aggregatorStub)
.withTreeWatcher(expectedWatcher); .withTreeRootRef(expectedTreeRootRef);
// act // act
builder.call(); builder.call();
// assert // assert
const actualWatcher = aggregatorStub.treeWatcher; const actualTreeRootRef = aggregatorStub.treeRootRef;
expect(actualWatcher).to.equal(expectedWatcher); expect(actualTreeRootRef).to.equal(expectedTreeRootRef);
}); });
it('does not throw if node has no parent', () => { it('does not throw if node has no parent', () => {
// arrange // arrange
@@ -185,21 +185,21 @@ describe('useAutoUpdateParentCheckState', () => {
class UseAutoUpdateParentCheckStateBuilder { class UseAutoUpdateParentCheckStateBuilder {
private changeAggregator = new UseNodeStateChangeAggregatorStub(); private changeAggregator = new UseNodeStateChangeAggregatorStub();
private treeWatcher: WatchSource<TreeRoot | undefined> = () => new TreeRootStub(); private treeRootRef: Readonly<Ref<TreeRoot>> = shallowRef(new TreeRootStub());
public withChangeAggregator(changeAggregator: UseNodeStateChangeAggregatorStub): this { public withChangeAggregator(changeAggregator: UseNodeStateChangeAggregatorStub): this {
this.changeAggregator = changeAggregator; this.changeAggregator = changeAggregator;
return this; return this;
} }
public withTreeWatcher(treeWatcher: WatchSource<TreeRoot | undefined>): this { public withTreeRootRef(treeRootRef: Readonly<Ref<TreeRoot>>): this {
this.treeWatcher = treeWatcher; this.treeRootRef = treeRootRef;
return this; return this;
} }
public call(): ReturnType<typeof useAutoUpdateParentCheckState> { public call(): ReturnType<typeof useAutoUpdateParentCheckState> {
return useAutoUpdateParentCheckState( return useAutoUpdateParentCheckState(
this.treeWatcher, this.treeRootRef,
this.changeAggregator.get(), this.changeAggregator.get(),
); );
} }

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { import {
shallowRef, defineComponent, WatchSource, nextTick, shallowRef, defineComponent, nextTick, type Ref,
} from 'vue'; } from 'vue';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot'; import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
@@ -15,25 +15,25 @@ describe('useCurrentTreeNodes', () => {
it('should set nodes on immediate invocation', () => { it('should set nodes on immediate invocation', () => {
// arrange // arrange
const expectedNodes = new QueryableNodesStub(); const expectedNodes = new QueryableNodesStub();
const treeWatcher = shallowRef<TreeRoot>(new TreeRootStub().withCollection( const treeRootRef = shallowRef(new TreeRootStub().withCollection(
new TreeNodeCollectionStub().withNodes(expectedNodes), new TreeNodeCollectionStub().withNodes(expectedNodes),
)); ));
// act // act
const { returnObject } = mountWrapperComponent(treeWatcher); const { returnObject } = mountWrapperComponent(treeRootRef);
// assert // assert
expect(returnObject.nodes.value).to.deep.equal(expectedNodes); expect(returnObject.nodes.value).to.deep.equal(expectedNodes);
}); });
it('should update nodes when treeWatcher changes', async () => { it('should update nodes when tree root changes', async () => {
// arrange // arrange
const initialNodes = new QueryableNodesStub(); const initialNodes = new QueryableNodesStub();
const treeWatcher = shallowRef( const treeRootRef = shallowRef(
new TreeRootStub().withCollection(new TreeNodeCollectionStub().withNodes(initialNodes)), new TreeRootStub().withCollection(new TreeNodeCollectionStub().withNodes(initialNodes)),
); );
const { returnObject } = mountWrapperComponent(treeWatcher); const { returnObject } = mountWrapperComponent(treeRootRef);
const newExpectedNodes = new QueryableNodesStub(); const newExpectedNodes = new QueryableNodesStub();
// act // act
treeWatcher.value = new TreeRootStub().withCollection( treeRootRef.value = new TreeRootStub().withCollection(
new TreeNodeCollectionStub().withNodes(newExpectedNodes), new TreeNodeCollectionStub().withNodes(newExpectedNodes),
); );
await nextTick(); await nextTick();
@@ -45,9 +45,9 @@ describe('useCurrentTreeNodes', () => {
// arrange // arrange
const initialNodes = new QueryableNodesStub(); const initialNodes = new QueryableNodesStub();
const treeCollectionStub = new TreeNodeCollectionStub().withNodes(initialNodes); const treeCollectionStub = new TreeNodeCollectionStub().withNodes(initialNodes);
const treeWatcher = shallowRef(new TreeRootStub().withCollection(treeCollectionStub)); const treeRootRef = shallowRef(new TreeRootStub().withCollection(treeCollectionStub));
const { returnObject } = mountWrapperComponent(treeWatcher); const { returnObject } = mountWrapperComponent(treeRootRef);
const newExpectedNodes = new QueryableNodesStub(); const newExpectedNodes = new QueryableNodesStub();
// act // act
@@ -58,12 +58,12 @@ describe('useCurrentTreeNodes', () => {
}); });
}); });
function mountWrapperComponent(treeWatcher: WatchSource<TreeRoot | undefined>) { function mountWrapperComponent(treeRootRef: Ref<TreeRoot>) {
let returnObject: ReturnType<typeof useCurrentTreeNodes>; let returnObject: ReturnType<typeof useCurrentTreeNodes>;
const wrapper = shallowMount( const wrapper = shallowMount(
defineComponent({ defineComponent({
setup() { setup() {
returnObject = useCurrentTreeNodes(treeWatcher); returnObject = useCurrentTreeNodes(treeRootRef);
}, },
template: '<div></div>', template: '<div></div>',
}), }),

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { WatchSource, defineComponent, nextTick } from 'vue'; import { defineComponent, nextTick, shallowRef } from 'vue';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot'; import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes'; import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes';
@@ -17,20 +17,21 @@ import { TreeNodeStateDescriptorStub } from '@tests/unit/shared/Stubs/TreeNodeSt
import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub'; import { TreeNodeStateAccessStub } from '@tests/unit/shared/Stubs/TreeNodeStateAccessStub';
import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection'; import { IEventSubscriptionCollection } from '@/infrastructure/Events/IEventSubscriptionCollection';
import { FunctionKeys } from '@/TypeHelpers'; import { FunctionKeys } from '@/TypeHelpers';
import type { Ref } from 'vue';
describe('useNodeStateChangeAggregator', () => { describe('useNodeStateChangeAggregator', () => {
it('watches nodes on specified tree', () => { it('tracks nodes on specified tree', () => {
// arrange // arrange
const expectedWatcher = () => new TreeRootStub(); const expectedTreeRootRef = shallowRef(new TreeRootStub());
const currentTreeNodesStub = new UseCurrentTreeNodesStub(); const currentTreeNodesStub = new UseCurrentTreeNodesStub();
const builder = new UseNodeStateChangeAggregatorBuilder() const builder = new UseNodeStateChangeAggregatorBuilder()
.withCurrentTreeNodes(currentTreeNodesStub.get()) .withCurrentTreeNodes(currentTreeNodesStub.get())
.withTreeWatcher(expectedWatcher); .withTreeRootRef(expectedTreeRootRef);
// act // act
builder.mountWrapperComponent(); builder.mountWrapperComponent();
// assert // assert
const actualWatcher = currentTreeNodesStub.treeWatcher; const actualTreeRootRef = currentTreeNodesStub.treeRootRef;
expect(actualWatcher).to.equal(expectedWatcher); expect(actualTreeRootRef).to.equal(expectedTreeRootRef);
}); });
describe('onNodeStateChange', () => { describe('onNodeStateChange', () => {
describe('throws if callback is absent', () => { describe('throws if callback is absent', () => {
@@ -302,14 +303,14 @@ function createFlatCollection(nodes: readonly TreeNode[]): QueryableNodesStub {
} }
class UseNodeStateChangeAggregatorBuilder { class UseNodeStateChangeAggregatorBuilder {
private treeWatcher: WatchSource<TreeRoot | undefined> = () => new TreeRootStub(); private treeRootRef: Readonly<Ref<TreeRoot>> = shallowRef(new TreeRootStub());
private currentTreeNodes: typeof useCurrentTreeNodes = new UseCurrentTreeNodesStub().get(); private currentTreeNodes: typeof useCurrentTreeNodes = new UseCurrentTreeNodesStub().get();
private events: UseAutoUnsubscribedEventsStub = new UseAutoUnsubscribedEventsStub(); private events: UseAutoUnsubscribedEventsStub = new UseAutoUnsubscribedEventsStub();
public withTreeWatcher(treeWatcher: WatchSource<TreeRoot | undefined>): this { public withTreeRootRef(treeRootRef: Readonly<Ref<TreeRoot>>): this {
this.treeWatcher = treeWatcher; this.treeRootRef = treeRootRef;
return this; return this;
} }
@@ -325,11 +326,11 @@ class UseNodeStateChangeAggregatorBuilder {
public mountWrapperComponent() { public mountWrapperComponent() {
let returnObject: ReturnType<typeof useNodeStateChangeAggregator>; let returnObject: ReturnType<typeof useNodeStateChangeAggregator>;
const { treeWatcher, currentTreeNodes } = this; const { treeRootRef, currentTreeNodes } = this;
const wrapper = shallowMount( const wrapper = shallowMount(
defineComponent({ defineComponent({
setup() { setup() {
returnObject = useNodeStateChangeAggregator(treeWatcher, currentTreeNodes); returnObject = useNodeStateChangeAggregator(treeRootRef, currentTreeNodes);
}, },
template: '<div></div>', template: '<div></div>',
}), }),

View File

@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { WatchSource, ref, nextTick } from 'vue'; import { ref, nextTick, type Ref } from 'vue';
import { CategoryNodeParser, useTreeViewNodeInput } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseTreeViewNodeInput'; import { CategoryNodeParser, useTreeViewNodeInput } from '@/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseTreeViewNodeInput';
import { InjectionKeys } from '@/presentation/injectionSymbols'; import { InjectionKeys } from '@/presentation/injectionSymbols';
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub'; import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
@@ -17,12 +17,10 @@ describe('useTreeViewNodeInput', () => {
describe('when given categoryId', () => { describe('when given categoryId', () => {
it('sets input nodes correctly', async () => { it('sets input nodes correctly', async () => {
// arrange // arrange
const testCategoryId = ref<number | undefined>(); const testCategoryIdRef = ref<number | undefined>();
const { const {
useStateStub, returnObject, parserMock, converterMock, useStateStub, returnObject, parserMock, converterMock,
} = mountWrapperComponent( } = mountWrapperComponent(testCategoryIdRef);
() => testCategoryId.value,
);
const expectedCategoryId = 123; const expectedCategoryId = 123;
const expectedCategoryCollection = new CategoryCollectionStub().withAction( const expectedCategoryCollection = new CategoryCollectionStub().withAction(
new CategoryStub(expectedCategoryId), new CategoryStub(expectedCategoryId),
@@ -45,7 +43,7 @@ describe('useTreeViewNodeInput', () => {
); );
// act // act
const { treeViewInputNodes } = returnObject; const { treeViewInputNodes } = returnObject;
testCategoryId.value = expectedCategoryId; testCategoryIdRef.value = expectedCategoryId;
await nextTick(); await nextTick();
// assert // assert
const actualInputNodes = treeViewInputNodes.value; const actualInputNodes = treeViewInputNodes.value;
@@ -60,9 +58,7 @@ describe('useTreeViewNodeInput', () => {
const testCategoryId = ref<number | undefined>(); const testCategoryId = ref<number | undefined>();
const { const {
useStateStub, returnObject, parserMock, converterMock, useStateStub, returnObject, parserMock, converterMock,
} = mountWrapperComponent( } = mountWrapperComponent(testCategoryId);
() => testCategoryId.value,
);
const expectedCategoryCollection = new CategoryCollectionStub().withAction( const expectedCategoryCollection = new CategoryCollectionStub().withAction(
new CategoryStub(123), new CategoryStub(123),
); );
@@ -92,7 +88,7 @@ describe('useTreeViewNodeInput', () => {
}); });
}); });
function mountWrapperComponent(categoryIdWatcher: WatchSource<number | undefined>) { function mountWrapperComponent(categoryIdRef: Ref<number | undefined>) {
const useStateStub = new UseCollectionStateStub(); const useStateStub = new UseCollectionStateStub();
const parserMock = mockCategoryNodeParser(); const parserMock = mockCategoryNodeParser();
const converterMock = mockConverter(); const converterMock = mockConverter();
@@ -100,7 +96,7 @@ function mountWrapperComponent(categoryIdWatcher: WatchSource<number | undefined
shallowMount({ shallowMount({
setup() { setup() {
returnObject = useTreeViewNodeInput(categoryIdWatcher, parserMock.mock, converterMock.mock); returnObject = useTreeViewNodeInput(categoryIdRef, parserMock.mock, converterMock.mock);
}, },
template: '<div></div>', template: '<div></div>',
}, { }, {

View File

@@ -1,5 +1,5 @@
import { import {
WatchSource, shallowReadonly, shallowRef, triggerRef, type Ref, shallowReadonly, shallowRef, triggerRef,
} from 'vue'; } from 'vue';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot'; import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes'; import { useCurrentTreeNodes } from '@/presentation/components/Scripts/View/Tree/TreeView/UseCurrentTreeNodes';
@@ -7,7 +7,7 @@ import { QueryableNodes } from '@/presentation/components/Scripts/View/Tree/Tree
import { QueryableNodesStub } from './QueryableNodesStub'; import { QueryableNodesStub } from './QueryableNodesStub';
export class UseCurrentTreeNodesStub { export class UseCurrentTreeNodesStub {
public treeWatcher: WatchSource<TreeRoot> | undefined; public treeRootRef: Readonly<Ref<TreeRoot>> | undefined;
private nodes = shallowRef<QueryableNodes>(new QueryableNodesStub()); private nodes = shallowRef<QueryableNodes>(new QueryableNodesStub());
@@ -22,8 +22,8 @@ export class UseCurrentTreeNodesStub {
} }
public get(): typeof useCurrentTreeNodes { public get(): typeof useCurrentTreeNodes {
return (treeWatcher: WatchSource<TreeRoot>) => { return (treeRootRef: Readonly<Ref<TreeRoot>>) => {
this.treeWatcher = treeWatcher; this.treeRootRef = treeRootRef;
return { return {
nodes: shallowReadonly(this.nodes), nodes: shallowReadonly(this.nodes),
}; };

View File

@@ -1,15 +1,15 @@
import { WatchSource } from 'vue';
import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot'; import { TreeRoot } from '@/presentation/components/Scripts/View/Tree/TreeView/TreeRoot/TreeRoot';
import { import {
NodeStateChangeEventArgs, NodeStateChangeEventArgs,
NodeStateChangeEventCallback, NodeStateChangeEventCallback,
useNodeStateChangeAggregator, useNodeStateChangeAggregator,
} from '@/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator'; } from '@/presentation/components/Scripts/View/Tree/TreeView/UseNodeStateChangeAggregator';
import type { Ref } from 'vue';
export class UseNodeStateChangeAggregatorStub { export class UseNodeStateChangeAggregatorStub {
public callback: NodeStateChangeEventCallback | undefined; public callback: NodeStateChangeEventCallback | undefined;
public treeWatcher: WatchSource<TreeRoot> | undefined; public treeRootRef: Readonly<Ref<TreeRoot>> | undefined;
public onNodeStateChange(callback: NodeStateChangeEventCallback) { public onNodeStateChange(callback: NodeStateChangeEventCallback) {
this.callback = callback; this.callback = callback;
@@ -23,8 +23,8 @@ export class UseNodeStateChangeAggregatorStub {
} }
public get(): typeof useNodeStateChangeAggregator { public get(): typeof useNodeStateChangeAggregator {
return (treeWatcher: WatchSource<TreeRoot>) => { return (treeRootRef: Readonly<Ref<TreeRoot>>) => {
this.treeWatcher = treeWatcher; this.treeRootRef = treeRootRef;
return { return {
onNodeStateChange: this.onNodeStateChange.bind(this), onNodeStateChange: this.onNodeStateChange.bind(this),
}; };