Skip to content

Commit

Permalink
Extract my watchEffect() trick into a general util function
Browse files Browse the repository at this point in the history
I will need a way to run some code when a ref becomes not-undefined, so
extract this into its own function.
  • Loading branch information
josh-berry committed Aug 19, 2024
1 parent 3cfba3c commit 0036c4c
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 11 deletions.
14 changes: 3 additions & 11 deletions src/components/search-input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
</template>

<script lang="ts">
import {ref, watch, watchEffect, type WatchStopHandle} from "vue";
import {ref, watch} from "vue";
import {onceRefHasValue} from "../util/index.js";
</script>

<script setup lang="ts">
Expand Down Expand Up @@ -68,16 +69,7 @@ const $search = ref(undefined! as HTMLInputElement);
defineExpose({
focus() {
// The watchEffect() is needed to avoid a race between the parent component
// being mounted and asking us to focus ourselves, and the actual mounting
// of this component.
let cancel: WatchStopHandle | undefined = undefined;
cancel = watchEffect(() => {
if ($search.value) {
$search.value.focus();
setTimeout(() => cancel!());
}
});
onceRefHasValue($search, s => s.focus());
},
});
Expand Down
30 changes: 30 additions & 0 deletions src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,36 @@ export async function resolveNamed<T extends {[k: string]: any}>(
export const later: <F extends () => any>(f: F) => void =
(<any>globalThis).setImmediate ?? globalThis.setTimeout;

/** Waits for a Vue ref to become not-undefined, and then runs its function
* once. (Note that the function is NOT re-run even if the ref becomes undefined
* and then not-undefined again.) */
export function onceRefHasValue<T>(
r: Vue.Ref<T | undefined | null>,
fn: (v: T) => void,
): Promise<void> {
return new Promise(resolve => {
let cancel: Vue.WatchStopHandle | undefined = undefined;
let cancelImmediately = false;

cancel = Vue.watchEffect(() => {
if (r.value === undefined || r.value === null) return;
fn(r.value);
resolve();

// The watchEffect() body is run synchronously the first time through, so
// cancel() might be undefined. If this happens, we need our caller to
// cancel us instead.
if (cancel) {
cancel();
} else {
cancelImmediately = true;
}
});

if (cancelImmediately) cancel();
});
}

/** Waits for the next iteration of the event loop and for Vue to flush any
* pending watches (allowing event handlers etc. to run in the meantime). */
export async function nextTick(): Promise<void> {
Expand Down

0 comments on commit 0036c4c

Please sign in to comment.