vueless 1.5.2-beta.1 → 1.5.2-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,7 +53,9 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
53
53
 
54
54
  /* Hoist shared reactive primitives — create once, share across all keys. */
55
55
  const attrs = useAttrs() as KeyAttrs;
56
- const reactiveAttrsClass = computed(() => attrs.class);
56
+
57
+ /* Reading `class` explicitly keeps attrs tracked even when they start empty. */
58
+ const reactiveAttrs = computed(() => ({ class: attrs.class, ...attrs }));
57
59
 
58
60
  /**
59
61
  * Reactive wrapper for props — created once per component instead of once per key.
@@ -235,6 +237,7 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
235
237
  * body row of a table on an unrelated sticky toggle). We skip the `.value` write
236
238
  * when the new object is structurally equal to the last one, so the ref keeps its
237
239
  * identity and dependents are not re-rendered.
240
+ * `JSON.stringify` drops functions, so listeners are compared by reference separately.
238
241
  */
239
242
  const attrsSignatures: Record<string, string> = {};
240
243
 
@@ -250,10 +253,10 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
250
253
  /**
251
254
  * Single consolidated watcher instead of N per-key watchers.
252
255
  * Watches: config (for config changes), reactiveProps (for prop changes),
253
- * mutatedProps (for slot/computed prop changes), reactiveAttrsClass (for class attr changes).
256
+ * mutatedProps (for slot/computed prop changes), reactiveAttrs (for attr changes).
254
257
  */
255
258
  watch(
256
- [config, reactiveProps, mutatedProps || (() => undefined), reactiveAttrsClass],
259
+ [config, reactiveProps, mutatedProps || (() => undefined), reactiveAttrs],
257
260
  () => {
258
261
  const derived = configDerivedData.value;
259
262
 
@@ -285,8 +288,10 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
285
288
 
286
289
  /* Keep the previous ref identity when nothing changed — see attrsSignatures. */
287
290
  const signature = JSON.stringify(nextValue);
291
+ const isUnchanged =
292
+ attrsSignatures[key] === signature && hasSameListeners(attrsRefs[key].value, nextValue);
288
293
 
289
- if (attrsSignatures[key] === signature) continue;
294
+ if (isUnchanged) continue;
290
295
 
291
296
  attrsSignatures[key] = signature;
292
297
  attrsRefs[key].value = nextValue;
@@ -312,6 +317,26 @@ export function useUI<T>(defaultConfig: T, mutatedProps?: MutatedProps, topLevel
312
317
  return { config, getDataTest, ...getKeysAttrs(mutatedProps) } as UseUI<T>;
313
318
  }
314
319
 
320
+ /**
321
+ * Check both attrs objects hold the same function-valued entries (listeners) by reference.
322
+ */
323
+ function hasSameListeners(previous: KeyAttrs, next: KeyAttrs): boolean {
324
+ let listenersDiff = 0;
325
+
326
+ for (const key in next) {
327
+ if (typeof next[key] !== "function") continue;
328
+ if (next[key] !== previous[key]) return false;
329
+
330
+ listenersDiff++;
331
+ }
332
+
333
+ for (const key in previous) {
334
+ if (typeof previous[key] === "function") listenersDiff--;
335
+ }
336
+
337
+ return listenersDiff === 0;
338
+ }
339
+
315
340
  /**
316
341
  * Return base classes.
317
342
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.5.2-beta.1",
3
+ "version": "1.5.2-beta.3",
4
4
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
5
5
  "author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
6
6
  "homepage": "https://vueless.com",
@@ -395,6 +395,23 @@ describe("UToggle.vue", () => {
395
395
  expect.objectContaining({ value: "option1", label: "Option 1" }),
396
396
  );
397
397
  });
398
+
399
+ it("Options – emits the new option value after options are swapped", async () => {
400
+ const component = mount(UToggle, {
401
+ props: {
402
+ options: [{ value: "code" }, { value: "self" }],
403
+ name,
404
+ },
405
+ });
406
+
407
+ await component.setProps({
408
+ options: [{ value: "git" }, { value: "jira" }, { value: "slack" }],
409
+ });
410
+
411
+ await component.findAllComponents(UButton)[1].trigger("click");
412
+
413
+ expect(component.emitted("update:modelValue")?.[0][0]).toBe("jira");
414
+ });
398
415
  });
399
416
 
400
417
  describe("Exposed refs", () => {
@@ -1,9 +1,7 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
1
+ import { describe, it, expect, vi } from "vitest";
2
2
 
3
3
  import { COLOR_MODE_KEY } from "../../constants.js";
4
- import { loadSSRVuelessConfig, setVuelessConfig, vuelessConfig } from "../ui";
5
-
6
- import type { Config } from "../../types";
4
+ import { setVuelessConfig } from "../ui";
7
5
 
8
6
  /* jsdom doesn't implement matchMedia, which theme.ts evaluates on import. */
9
7
  const prefersDark = { matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() };
@@ -12,37 +10,17 @@ window.matchMedia = vi.fn(() => prefersDark) as unknown as typeof window.matchMe
12
10
 
13
11
  const { getThemeCookieName } = await import("../theme");
14
12
 
15
- beforeEach(() => {
16
- Object.keys(vuelessConfig).forEach((key) => delete vuelessConfig[key as keyof Config]);
17
- });
18
-
19
- describe("loadSSRVuelessConfig", () => {
20
- it.each([
21
- ["resolves", () => ({ cookiePrefix: "from-file" })],
22
- [
23
- "fails",
24
- () => {
25
- throw new Error("Cannot find module '/vueless.config'");
26
- },
27
- ],
28
- ])("keeps the injected config when a deferred import %s", async (_, settle: () => Config) => {
29
- let release!: () => void;
30
- const released = new Promise<void>((resolve) => (release = resolve));
31
-
32
- const loading = loadSSRVuelessConfig(() => released.then(() => ({ default: settle() })));
33
-
13
+ describe("setVuelessConfig", () => {
14
+ it("uses the injected config", () => {
34
15
  setVuelessConfig({ cookiePrefix: "app-a" });
35
- expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`app-a-${COLOR_MODE_KEY}`);
36
-
37
- release();
38
- await loading;
39
16
 
40
17
  expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`app-a-${COLOR_MODE_KEY}`);
41
18
  });
42
19
 
43
- it("uses the imported config when none was injected", async () => {
44
- await loadSSRVuelessConfig(async () => ({ default: { cookiePrefix: "from-file" } }));
20
+ it("keeps the current config when an empty one is injected", () => {
21
+ setVuelessConfig({ cookiePrefix: "app-a" });
22
+ setVuelessConfig({});
45
23
 
46
- expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`from-file-${COLOR_MODE_KEY}`);
24
+ expect(getThemeCookieName(COLOR_MODE_KEY)).toBe(`app-a-${COLOR_MODE_KEY}`);
47
25
  });
48
26
  });
package/utils/ui.ts CHANGED
@@ -3,7 +3,6 @@ import { cx as cvaCx } from "cva";
3
3
  import { defineConfig } from "cva/config";
4
4
  import { extendTailwindMerge } from "tailwind-merge";
5
5
 
6
- import { isCSR, isSSR } from "./helper";
7
6
  import { createGetMergedConfig } from "./node/mergeConfigs";
8
7
  import { COMPONENT_NAME as U_ICON } from "../ui.image-icon/constants";
9
8
  import { ICON_NON_PROPS_DEFAULTS, TAILWIND_MERGE_EXTENSION } from "../constants";
@@ -30,7 +29,13 @@ type GetMergedConfig = (options: MergedConfigOptions) => unknown;
30
29
  * Load Vueless config from the project root.
31
30
  * Both for server and client side renderings.
32
31
  */
33
- export let vuelessConfig: Config = {};
32
+ export let vuelessConfig: Config =
33
+ Object.values(
34
+ import.meta.glob(["/vueless.config.{js,ts}"], {
35
+ eager: true,
36
+ import: "default",
37
+ }),
38
+ )[0] || {};
34
39
 
35
40
  export function setVuelessConfig(config?: Config) {
36
41
  config = config || {};
@@ -38,32 +43,6 @@ export function setVuelessConfig(config?: Config) {
38
43
  vuelessConfig = Object.keys(config).length ? config : vuelessConfig;
39
44
  }
40
45
 
41
- if (isCSR) {
42
- vuelessConfig =
43
- Object.values(
44
- import.meta.glob(["/vueless.config.{js,ts}"], {
45
- eager: true,
46
- import: "default",
47
- }),
48
- )[0] || {};
49
- }
50
-
51
- /** Load Vueless config on the server; a config injected via `createVueless` meanwhile wins. */
52
- export async function loadSSRVuelessConfig(
53
- // @ts-expect-error: vueless.config.{js,ts} is optional
54
- importConfig = () => import(/* @vite-ignore */ "/vueless.config"),
55
- ) {
56
- try {
57
- const { default: config } = await importConfig();
58
-
59
- if (!Object.keys(vuelessConfig).length) vuelessConfig = config;
60
- } catch {
61
- /* No config file: keep the current config. */
62
- }
63
- }
64
-
65
- if (isSSR) loadSSRVuelessConfig();
66
-
67
46
  /**
68
47
  * Extend twMerge (tailwind merge) by vueless and user config:
69
48
  */