mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 19:04:46 +00:00
feat(web/vue): add useStorage and useStorageAsync, separate all composables by categories
This commit is contained in:
3
web/vue/src/composables/component/index.ts
Normal file
3
web/vue/src/composables/component/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './unrefElement';
|
||||
export * from './useRenderCount';
|
||||
export * from './useRenderInfo';
|
||||
61
web/vue/src/composables/component/unrefElement/index.test.ts
Normal file
61
web/vue/src/composables/component/unrefElement/index.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { computed, defineComponent, nextTick, ref, shallowRef } from 'vue';
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { unrefElement } from '.';
|
||||
|
||||
describe('unrefElement', () => {
|
||||
it('returns a plain element when passed a raw element', () => {
|
||||
const htmlEl = document.createElement('div');
|
||||
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
|
||||
expect(unrefElement(htmlEl)).toBe(htmlEl);
|
||||
expect(unrefElement(svgEl)).toBe(svgEl);
|
||||
});
|
||||
|
||||
it('returns element when passed a ref or shallowRef to an element', () => {
|
||||
const el = document.createElement('div');
|
||||
const elRef = ref<HTMLElement | null>(el);
|
||||
const shallowElRef = shallowRef<HTMLElement | null>(el);
|
||||
|
||||
expect(unrefElement(elRef)).toBe(el);
|
||||
expect(unrefElement(shallowElRef)).toBe(el);
|
||||
});
|
||||
|
||||
it('returns element when passed a computed ref or getter function', () => {
|
||||
const el = document.createElement('div');
|
||||
const computedElRef = computed(() => el);
|
||||
const elGetter = () => el;
|
||||
|
||||
expect(unrefElement(computedElRef)).toBe(el);
|
||||
expect(unrefElement(elGetter)).toBe(el);
|
||||
});
|
||||
|
||||
it('returns component $el when passed a component instance', async () => {
|
||||
const Child = defineComponent({
|
||||
template: `<span class="child-el">child</span>`,
|
||||
});
|
||||
|
||||
const Parent = defineComponent({
|
||||
components: { Child },
|
||||
template: `<Child ref="childRef" />`,
|
||||
});
|
||||
|
||||
const wrapper = mount(Parent);
|
||||
await nextTick();
|
||||
|
||||
const childInstance = (wrapper.vm as any).$refs.childRef;
|
||||
const result = unrefElement(childInstance);
|
||||
|
||||
expect(result).toBe(childInstance.$el);
|
||||
expect((result as HTMLElement).classList.contains('child-el')).toBe(true);
|
||||
});
|
||||
|
||||
it('handles null and undefined values', () => {
|
||||
expect(unrefElement(undefined)).toBe(undefined);
|
||||
expect(unrefElement(null)).toBe(null);
|
||||
expect(unrefElement(ref(null))).toBe(null);
|
||||
expect(unrefElement(ref(undefined))).toBe(undefined);
|
||||
expect(unrefElement(() => null)).toBe(null);
|
||||
expect(unrefElement(() => undefined)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
33
web/vue/src/composables/component/unrefElement/index.ts
Normal file
33
web/vue/src/composables/component/unrefElement/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { ComponentPublicInstance, MaybeRef, MaybeRefOrGetter } from 'vue';
|
||||
import { toValue } from 'vue';
|
||||
|
||||
export type VueInstance = ComponentPublicInstance;
|
||||
export type MaybeElement = HTMLElement | SVGElement | VueInstance | undefined | null;
|
||||
|
||||
export type MaybeElementRef<El extends MaybeElement = MaybeElement> = MaybeRef<El>;
|
||||
export type MaybeComputedElementRef<El extends MaybeElement = MaybeElement> = MaybeRefOrGetter<El>;
|
||||
|
||||
export type UnRefElementReturn<T extends MaybeElement = MaybeElement> = T extends VueInstance ? Exclude<MaybeElement, VueInstance> : T | undefined;
|
||||
|
||||
/**
|
||||
* @name unrefElement
|
||||
* @category Component
|
||||
* @description Unwraps a Vue element reference to get the underlying instance or DOM element.
|
||||
*
|
||||
* @param {MaybeComputedElementRef<El>} elRef - The element reference to unwrap.
|
||||
* @returns {UnRefElementReturn<El>} - The unwrapped element or undefined.
|
||||
*
|
||||
* @example
|
||||
* const element = useTemplateRef<HTMLElement>('element');
|
||||
* const result = unrefElement(element); // result is the element instance
|
||||
*
|
||||
* @example
|
||||
* const component = useTemplateRef<Component>('component');
|
||||
* const result = unrefElement(component); // result is the component instance
|
||||
*
|
||||
* @since 0.0.11
|
||||
*/
|
||||
export function unrefElement<El extends MaybeElement>(elRef: MaybeComputedElementRef<El>): UnRefElementReturn<El> {
|
||||
const plain = toValue(elRef);
|
||||
return (plain as VueInstance)?.$el ?? plain;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { defineComponent, nextTick, ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { useRenderCount } from '.';
|
||||
|
||||
const ComponentStub = defineComponent({
|
||||
setup() {
|
||||
const count = useRenderCount();
|
||||
const visibleCount = ref(0);
|
||||
const hiddenCount = ref(0);
|
||||
|
||||
return { count, visibleCount, hiddenCount };
|
||||
},
|
||||
template: `<div>{{ visibleCount }}</div>`,
|
||||
});
|
||||
|
||||
describe('useRenderCount', () => {
|
||||
it('return the number of times the component has been rendered', async () => {
|
||||
const component = mount(ComponentStub);
|
||||
|
||||
// Initial render
|
||||
expect(component.vm.count).toBe(1);
|
||||
|
||||
component.vm.hiddenCount = 1;
|
||||
await nextTick();
|
||||
|
||||
// Will not trigger a render
|
||||
expect(component.vm.count).toBe(1);
|
||||
expect(component.text()).toBe('0');
|
||||
|
||||
component.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
// Will trigger a render
|
||||
expect(component.vm.count).toBe(2);
|
||||
expect(component.text()).toBe('1');
|
||||
|
||||
component.vm.visibleCount++;
|
||||
component.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
// Will trigger a single render for both updates
|
||||
expect(component.vm.count).toBe(3);
|
||||
expect(component.text()).toBe('3');
|
||||
});
|
||||
|
||||
it('can be used with a specific component instance', async () => {
|
||||
const component = mount(ComponentStub);
|
||||
const instance = component.vm.$;
|
||||
|
||||
const count = useRenderCount(instance);
|
||||
|
||||
// Initial render (should be zero because the component has already rendered on mount)
|
||||
expect(count.value).toBe(0);
|
||||
|
||||
component.vm.hiddenCount = 1;
|
||||
await nextTick();
|
||||
|
||||
// Will not trigger a render
|
||||
expect(count.value).toBe(0);
|
||||
|
||||
component.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
// Will trigger a render
|
||||
expect(count.value).toBe(1);
|
||||
|
||||
component.vm.visibleCount++;
|
||||
component.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
// Will trigger a single render for both updates
|
||||
expect(count.value).toBe(2);
|
||||
});
|
||||
});
|
||||
29
web/vue/src/composables/component/useRenderCount/index.ts
Normal file
29
web/vue/src/composables/component/useRenderCount/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { onMounted, onUpdated, readonly, type ComponentInternalInstance } from 'vue';
|
||||
import { useCounter } from '@/composables/state/useCounter';
|
||||
import { getLifeCycleTarger } from '@/utils';
|
||||
|
||||
/**
|
||||
* @name useRenderCount
|
||||
* @category Component
|
||||
* @description Returns the number of times the component has been rendered into the DOM
|
||||
*
|
||||
* @param {ComponentInternalInstance} [instance] The component instance to track the render count for
|
||||
* @returns {Readonly<Ref<number>>} The number of times the component has been rendered
|
||||
*
|
||||
* @example
|
||||
* const count = useRenderCount();
|
||||
*
|
||||
* @example
|
||||
* const count = useRenderCount(getCurrentInstance());
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function useRenderCount(instance?: ComponentInternalInstance) {
|
||||
const { count, increment } = useCounter(0);
|
||||
const target = getLifeCycleTarger(instance);
|
||||
|
||||
onMounted(increment, target);
|
||||
onUpdated(increment, target);
|
||||
|
||||
return readonly(count);
|
||||
}
|
||||
100
web/vue/src/composables/component/useRenderInfo/index.test.ts
Normal file
100
web/vue/src/composables/component/useRenderInfo/index.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { useRenderInfo } from '.';
|
||||
import { defineComponent, nextTick, ref } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
|
||||
const NamedComponentStub = defineComponent({
|
||||
name: 'ComponentStub',
|
||||
setup() {
|
||||
const info = useRenderInfo();
|
||||
const visibleCount = ref(0);
|
||||
const hiddenCount = ref(0);
|
||||
|
||||
return { info, visibleCount, hiddenCount };
|
||||
},
|
||||
template: `<div>{{ visibleCount }}</div>`,
|
||||
});
|
||||
|
||||
const UnnamedComponentStub = defineComponent({
|
||||
setup() {
|
||||
const info = useRenderInfo();
|
||||
const visibleCount = ref(0);
|
||||
const hiddenCount = ref(0);
|
||||
|
||||
return { info, visibleCount, hiddenCount };
|
||||
},
|
||||
template: `<div>{{ visibleCount }}</div>`,
|
||||
});
|
||||
|
||||
describe('useRenderInfo', () => {
|
||||
it('return uid if component name is not available', async () => {
|
||||
const wrapper = mount(UnnamedComponentStub);
|
||||
|
||||
expect(wrapper.vm.info.component).toBe(wrapper.vm.$.uid);
|
||||
});
|
||||
|
||||
it('return render info for the given instance', async () => {
|
||||
const wrapper = mount(NamedComponentStub);
|
||||
|
||||
// Initial render
|
||||
expect(wrapper.vm.info.component).toBe('ComponentStub');
|
||||
expect(wrapper.vm.info.count.value).toBe(1);
|
||||
expect(wrapper.vm.info.duration.value).toBeGreaterThan(0);
|
||||
expect(wrapper.vm.info.lastRendered).toBeGreaterThan(0);
|
||||
|
||||
let lastRendered = wrapper.vm.info.lastRendered;
|
||||
let duration = wrapper.vm.info.duration.value;
|
||||
|
||||
// Will not trigger a render
|
||||
wrapper.vm.hiddenCount++;
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.info.component).toBe('ComponentStub');
|
||||
expect(wrapper.vm.info.count.value).toBe(1);
|
||||
expect(wrapper.vm.info.duration.value).toBe(duration);
|
||||
expect(wrapper.vm.info.lastRendered).toBe(lastRendered);
|
||||
|
||||
// Will trigger a render
|
||||
wrapper.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.info.component).toBe('ComponentStub');
|
||||
expect(wrapper.vm.info.count.value).toBe(2);
|
||||
expect(wrapper.vm.info.duration.value).not.toBe(duration);
|
||||
expect(wrapper.vm.info.lastRendered).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('can be used with a specific component instance', async () => {
|
||||
const wrapper = mount(NamedComponentStub);
|
||||
const instance = wrapper.vm.$;
|
||||
|
||||
const info = useRenderInfo(instance);
|
||||
|
||||
// Initial render (should be zero because the component has already rendered on mount)
|
||||
expect(info.component).toBe('ComponentStub');
|
||||
expect(info.count.value).toBe(0);
|
||||
expect(info.duration.value).toBe(0);
|
||||
expect(info.lastRendered).toBeGreaterThan(0);
|
||||
|
||||
let lastRendered = info.lastRendered;
|
||||
let duration = info.duration.value;
|
||||
|
||||
// Will not trigger a render
|
||||
wrapper.vm.hiddenCount++;
|
||||
await nextTick();
|
||||
|
||||
expect(info.component).toBe('ComponentStub');
|
||||
expect(info.count.value).toBe(0);
|
||||
expect(info.duration.value).toBe(duration);
|
||||
expect(info.lastRendered).toBe(lastRendered);
|
||||
|
||||
// Will trigger a render
|
||||
wrapper.vm.visibleCount++;
|
||||
await nextTick();
|
||||
|
||||
expect(info.component).toBe('ComponentStub');
|
||||
expect(info.count.value).toBe(1);
|
||||
expect(info.duration.value).not.toBe(duration);
|
||||
expect(info.lastRendered).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
45
web/vue/src/composables/component/useRenderInfo/index.ts
Normal file
45
web/vue/src/composables/component/useRenderInfo/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { timestamp } from '@robonen/stdlib';
|
||||
import { onBeforeMount, onBeforeUpdate, onMounted, onUpdated, readonly, ref, type ComponentInternalInstance } from 'vue';
|
||||
import { useRenderCount } from '../useRenderCount';
|
||||
import { getLifeCycleTarger } from '@/utils';
|
||||
|
||||
/**
|
||||
* @name useRenderInfo
|
||||
* @category Component
|
||||
* @description Returns information about the component's render count and the last time it was rendered
|
||||
*
|
||||
* @param {ComponentInternalInstance} [instance] The component instance to track the render count for
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const { component, count, duration, lastRendered } = useRenderInfo();
|
||||
*
|
||||
* @example
|
||||
* const { component, count, duration, lastRendered } = useRenderInfo(getCurrentInstance());
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export function useRenderInfo(instance?: ComponentInternalInstance) {
|
||||
const target = getLifeCycleTarger(instance);
|
||||
const duration = ref(0);
|
||||
let renderStartTime = 0;
|
||||
|
||||
const startMark = () => renderStartTime = performance.now();
|
||||
const endMark = () => {
|
||||
duration.value = Math.max(performance.now() - renderStartTime, 0);
|
||||
renderStartTime = 0;
|
||||
};
|
||||
|
||||
onBeforeMount(startMark, target);
|
||||
onMounted(endMark, target);
|
||||
|
||||
onBeforeUpdate(startMark, target);
|
||||
onUpdated(endMark, target);
|
||||
|
||||
return {
|
||||
component: target?.type.name ?? target?.uid,
|
||||
count: useRenderCount(instance),
|
||||
duration: readonly(duration),
|
||||
lastRendered: timestamp(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user