@lovrozagar/flare 0.9.12 → 0.9.14

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.
package/README.md CHANGED
@@ -380,6 +380,8 @@ export const route = createLayout("_root_/(blog)")
380
380
  ));
381
381
  ```
382
382
 
383
+ A layout stays mounted while navigation changes only params its own path does not declare (`/blog/a` → `/blog/b` keeps `(blog)`; `[locale]` layouts remount on a locale switch). Its loader still re-runs and `props.loaderData` updates reactively, so read data in JSX or memos, not once in the render body. Pages remount on any param change.
384
+
383
385
  ```ts
384
386
  import { createPathSegment } from "@lovrozagar/flare/path-segment";
385
387
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovrozagar/flare",
3
- "version": "0.9.12",
3
+ "version": "0.9.14",
4
4
  "description": "Solid meta-framework. Server-driven, NDJSON streaming, renderToStream.",
5
5
  "keywords": [
6
6
  "flare",
@@ -50,8 +50,9 @@ interface FormOwnProps<TInput, TOutput> {
50
50
  onSuccess?: (data: TOutput) => void;
51
51
  }
52
52
 
53
+ /* The element's own `onError` (an ErrorEvent) gives way to the action's. */
53
54
  export type FormProps<TInput, TOutput> = FormOwnProps<TInput, TOutput> &
54
- Omit<JSX.FormHTMLAttributes<HTMLFormElement>, "action" | "children" | "enctype" | "method">;
55
+ Omit<JSX.FormHTMLAttributes<HTMLFormElement>, "action" | "children" | "enctype" | "method" | "onError">;
55
56
 
56
57
  /** Seed `form.error()` after a no-JS PE POST with form-level validation errors. */
57
58
  export function seedFormErrorFromSsr(ssrCtx: FormActionContext | undefined): Error | null {
@@ -66,7 +67,7 @@ export function seedFormErrorFromSsr(ssrCtx: FormActionContext | undefined): Err
66
67
 
67
68
  export function Form<TInput, TOutput>(props: FormProps<TInput, TOutput>): JSX.Element {
68
69
  const local = props as FormOwnProps<TInput, TOutput> & Record<string, unknown>;
69
- const rest = omit(local, "action", "children", "enctype", "onError", "onSuccess");
70
+ const rest = omit(local, "action", "children", "enctype", "onError", "onSubmit", "onSuccess");
70
71
 
71
72
  const reg = createMemo(() => local.action._registration);
72
73
  const fnId = createMemo(() => reg()?.id ?? "");
@@ -107,6 +108,12 @@ export function Form<TInput, TOutput>(props: FormProps<TInput, TOutput>): JSX.El
107
108
  };
108
109
 
109
110
  async function handleSubmit(event: SubmitEvent): Promise<void> {
111
+ /* The consumer's handler runs first (client validation, e.g. Base UI Form composed through
112
+ `render`); preventing the default cancels the server call. */
113
+ const consumer = props.onSubmit as JSX.EventHandlerUnion<HTMLFormElement, SubmitEvent> | undefined;
114
+ if (typeof consumer === "function") consumer(event as never);
115
+ else if (Array.isArray(consumer)) (consumer[0] as (data: unknown, event: SubmitEvent) => void)(consumer[1], event);
116
+ if (event.defaultPrevented) return;
110
117
  event.preventDefault();
111
118
  if (pending()) return;
112
119
 
@@ -42,6 +42,7 @@ import { executeRewriteInput, executeRewriteOutput, type LocationRewrite } from
42
42
  import type { HeadConfig } from "../route-builder/types.ts";
43
43
  import {
44
44
  computeMatchId,
45
+ deriveParams,
45
46
  isRootLayoutPath,
46
47
  matchRoute,
47
48
  matchRoutePartial,
@@ -629,10 +630,13 @@ function commitCachedShell(
629
630
  return { hadShell: true, keepMatchIds };
630
631
  }
631
632
 
632
- function sameRouteParams(a: Record<string, string | string[]>, b: Record<string, string | string[]>): boolean {
633
- const keys = Object.keys(a);
634
- if (keys.length !== Object.keys(b).length) return false;
635
- for (const key of keys) {
633
+ /** Whether the params a route's own path declares kept their values. */
634
+ function ownParamsUnchanged(
635
+ virtualPath: string,
636
+ a: Record<string, string | string[]>,
637
+ b: Record<string, string | string[]>,
638
+ ): boolean {
639
+ for (const key of deriveParams(virtualPath)) {
636
640
  const av = a[key];
637
641
  const bv = b[key];
638
642
  if (av === bv) continue;
@@ -655,7 +659,7 @@ function buildClientMatches(
655
659
  params: Record<string, string | string[]>,
656
660
  ) {
657
661
  const current = ctx?.matches() ?? [];
658
- const paramsUnchanged = sameRouteParams(ctx?.params() ?? {}, params);
662
+ const prevParams = ctx?.params() ?? {};
659
663
  return allModules.map((mod, i) => {
660
664
  const matchId = matchIdForModule(mod, search, params);
661
665
  const cached = ctx?.matchCache.get(matchId);
@@ -676,15 +680,17 @@ function buildClientMatches(
676
680
  };
677
681
  /* Reuse the previous object when the route slot is unchanged so
678
682
  Outlet <Show keyed when={match()}> does not remount (local page
679
- signals). Param changes are a new page instance — render bodies
680
- that snapshot loaderData (translators, query keys) must re-run.
681
- Search-only updates keep identity and refresh via matchesEpoch.
682
- Error identity must change — Errored stays in fallback if the same
683
- object is mutated from error to success. */
683
+ signals). A change to a param the route's own path declares is a
684
+ new instance — render bodies that snapshot loaderData (translators,
685
+ query keys) must re-run. A layout whose own params did not change
686
+ stays mounted when a child param changes; like search-only updates,
687
+ its data refreshes via matchesEpoch. Error identity must change —
688
+ Errored stays in fallback if the same object is mutated from error
689
+ to success. */
684
690
  const prev = current[i];
685
691
  if (
686
692
  prev &&
687
- paramsUnchanged &&
693
+ ownParamsUnchanged(mod.virtualPath, prevParams, params) &&
688
694
  prev.virtualPath === next.virtualPath &&
689
695
  prev._type === next._type &&
690
696
  prev.error === next.error