@hybridly/vue 0.0.1-alpha.13 → 0.0.1-alpha.15

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/dist/index.cjs CHANGED
@@ -23,13 +23,6 @@ const state = {
23
23
  viewKey: vue.ref(),
24
24
  dialog: vue.shallowRef(),
25
25
  dialogKey: vue.ref(),
26
- routes: vue.ref(),
27
- setRoutes(routes) {
28
- utils.debug.adapter("vue:state:routes", "Setting routes:", routes);
29
- if (routes) {
30
- state.routes.value = vue.unref(routes);
31
- }
32
- },
33
26
  setView(view) {
34
27
  utils.debug.adapter("vue:state:view", "Setting view:", view);
35
28
  state.view.value = view;
@@ -63,13 +56,12 @@ const state = {
63
56
 
64
57
  const wrapper = vue.defineComponent({
65
58
  name: "Hybridly",
66
- setup(props) {
67
- if (typeof window !== "undefined") {
68
- state.setContext(props.context);
69
- if (!props.context) {
70
- throw new Error("Hybridly was not properly initialized. The context is missing.");
71
- }
72
- }
59
+ setup() {
60
+ const properties = vue.ref();
61
+ vue.watch(() => state.context.value?.view?.properties, (value) => {
62
+ utils.debug.adapter("vue:properties", "Updating properties.", vue.toRaw(value));
63
+ properties.value = value;
64
+ }, { immediate: true });
73
65
  function renderLayout(child) {
74
66
  utils.debug.adapter("vue:render:layout", "Rendering layout.");
75
67
  if (typeof state.view.value?.layout === "function") {
@@ -80,14 +72,14 @@ const wrapper = vue.defineComponent({
80
72
  layout.inheritAttrs = !!layout.inheritAttrs;
81
73
  return vue.h(layout, {
82
74
  ...state.view.value?.layoutProperties ?? {},
83
- ...state.context.value.view.properties
75
+ ...properties.value
84
76
  }, () => child2);
85
77
  });
86
78
  }
87
79
  return [
88
80
  vue.h(state.view.value?.layout, {
89
81
  ...state.view.value?.layoutProperties ?? {},
90
- ...state.context.value.view.properties
82
+ ...properties.value
91
83
  }, () => child),
92
84
  renderDialog()
93
85
  ];
@@ -96,7 +88,7 @@ const wrapper = vue.defineComponent({
96
88
  utils.debug.adapter("vue:render:view", "Rendering view.");
97
89
  state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
98
90
  return vue.h(state.view.value, {
99
- ...state.context.value.view.properties,
91
+ ...properties.value,
100
92
  key: state.viewKey.value
101
93
  });
102
94
  }
@@ -109,29 +101,24 @@ const wrapper = vue.defineComponent({
109
101
  });
110
102
  }
111
103
  }
112
- return () => {
113
- if (state.view.value) {
114
- const view = renderView();
115
- if (state.viewLayout.value) {
116
- state.view.value.layout = state.viewLayout.value;
117
- state.viewLayout.value = void 0;
118
- }
119
- if (state.viewLayoutProperties.value) {
120
- state.view.value.layoutProperties = state.viewLayoutProperties.value;
121
- state.viewLayoutProperties.value = void 0;
122
- }
123
- if (state.view.value.layout) {
124
- return renderLayout(view);
125
- }
126
- return [view, renderDialog()];
104
+ return (...a) => {
105
+ if (!state.view.value) {
106
+ return;
107
+ }
108
+ utils.debug.adapter("vue:render:wrapper", "Rendering wrapper component.", a.map(vue.toRaw));
109
+ const view = renderView();
110
+ if (state.viewLayout.value) {
111
+ state.view.value.layout = state.viewLayout.value;
127
112
  }
113
+ if (state.viewLayoutProperties.value) {
114
+ state.view.value.layoutProperties = state.viewLayoutProperties.value;
115
+ state.viewLayoutProperties.value = void 0;
116
+ }
117
+ if (state.view.value.layout) {
118
+ return renderLayout(view);
119
+ }
120
+ return [view, renderDialog()];
128
121
  };
129
- },
130
- props: {
131
- context: {
132
- type: Object,
133
- required: true
134
- }
135
122
  }
136
123
  });
137
124
 
@@ -173,8 +160,8 @@ function setupDevtools(app) {
173
160
  });
174
161
  payload.instanceData.state.push({
175
162
  type: hybridlyStateType,
176
- key: "router",
177
- value: state.routes.value
163
+ key: "routing",
164
+ value: state.context.value?.routing
178
165
  });
179
166
  });
180
167
  api.on.editComponentState((payload) => {
@@ -231,7 +218,7 @@ function setupDevtools(app) {
231
218
  });
232
219
  });
233
220
  }
234
- const plugin = {
221
+ const devtools = {
235
222
  install(app) {
236
223
  if (process.env.NODE_ENV === "development" || __VUE_PROD_DEVTOOLS__) {
237
224
  setupDevtools(app);
@@ -267,18 +254,26 @@ async function initializeHybridly(options) {
267
254
  },
268
255
  payload
269
256
  }));
270
- const render = () => vue.h(wrapper, { context: state.context.value });
257
+ if (typeof window !== "undefined") {
258
+ window.addEventListener("hybridly:routing", (event) => {
259
+ state.context.value?.adapter.updateRoutingConfiguration(event.detail);
260
+ });
261
+ window.dispatchEvent(new CustomEvent("hybridly:routing", { detail: window?.hybridly?.routing }));
262
+ }
263
+ const render = () => vue.h(wrapper);
271
264
  if (options.setup) {
272
265
  return await options.setup({
273
266
  element,
274
267
  wrapper,
275
268
  render,
276
- hybridly: plugin,
269
+ hybridly: devtools,
277
270
  props: { context: state.context.value }
278
271
  });
279
272
  }
280
273
  const app = vue.createApp({ render });
281
- app.use(plugin);
274
+ if (options.devtools !== false) {
275
+ app.use(devtools);
276
+ }
282
277
  await options.enhanceVue?.(app);
283
278
  return app.mount(element);
284
279
  }
@@ -304,14 +299,6 @@ function prepare(options) {
304
299
  }
305
300
  throw new Error("Either `initializeHybridly#resolve` or `initializeHybridly#pages` should be defined.");
306
301
  };
307
- if (typeof window !== "undefined") {
308
- state.setRoutes(window?.hybridly?.routes);
309
- window.addEventListener("hybridly:routes", (event) => {
310
- if (event.detail) {
311
- state.setRoutes(event.detail);
312
- }
313
- });
314
- }
315
302
  if (options.progress !== false) {
316
303
  options.plugins = [
317
304
  progressPlugin.progress(typeof options.progress === "object" ? options.progress : {}),
@@ -439,12 +426,12 @@ const HybridlyImports = {
439
426
  "useHistoryState",
440
427
  "usePaginator",
441
428
  "defineLayout",
442
- "defineLayoutProperties",
443
- "route"
429
+ "defineLayoutProperties"
444
430
  ],
445
431
  "hybridly": [
446
432
  "registerHook",
447
433
  "router",
434
+ "route",
448
435
  "can"
449
436
  ]
450
437
  };
@@ -521,7 +508,7 @@ function safeClone(obj) {
521
508
  return utils.clone(vue.toRaw(obj));
522
509
  }
523
510
  function useForm(options) {
524
- const shouldRemember = options?.key !== false;
511
+ const shouldRemember = !!options?.key;
525
512
  const historyKey = options?.key ?? "form:default";
526
513
  const historyData = shouldRemember ? core.router.history.get(historyKey) : void 0;
527
514
  const timeoutIds = {
@@ -551,13 +538,14 @@ function useForm(options) {
551
538
  function submit(optionsOverrides) {
552
539
  const url = typeof options.url === "function" ? options.url() : options.url;
553
540
  const data = typeof options.transform === "function" ? options.transform?.(fields) : fields;
541
+ const preserveState = optionsOverrides?.preserveState ?? options.preserveState;
554
542
  return core.router.navigate({
555
543
  ...options,
556
544
  url: url ?? state.context.value?.url,
557
545
  method: options.method ?? "POST",
558
546
  ...optionsOverrides,
559
547
  data: safeClone(data),
560
- preserveState: optionsOverrides?.preserveState === void 0 && options.method !== "GET" ? true : optionsOverrides?.preserveState,
548
+ preserveState: preserveState === void 0 && options.method !== "GET" ? true : preserveState,
561
549
  hooks: {
562
550
  before: (navigation, context) => {
563
551
  failed.value = false;
@@ -565,7 +553,6 @@ function useForm(options) {
565
553
  recentlySuccessful.value = false;
566
554
  clearTimeout(timeoutIds.recentlySuccessful);
567
555
  clearTimeout(timeoutIds.recentlyFailed);
568
- clearErrors();
569
556
  return options.hooks?.before?.(navigation, context);
570
557
  },
571
558
  start: (context) => {
@@ -584,6 +571,7 @@ function useForm(options) {
584
571
  return options.hooks?.error?.(incoming, context);
585
572
  },
586
573
  success: (payload, context) => {
574
+ clearErrors();
587
575
  if (options?.reset !== false) {
588
576
  reset();
589
577
  }
@@ -608,6 +596,12 @@ function useForm(options) {
608
596
  clearError(key);
609
597
  });
610
598
  }
599
+ function hasDirty(...keys) {
600
+ if (keys.length === 0) {
601
+ return isDirty.value;
602
+ }
603
+ return keys.some((key) => !isEqual__default(vue.toRaw(fields[key]), vue.toRaw(initial[key])));
604
+ }
611
605
  function clearError(key) {
612
606
  errors.value[key] = void 0;
613
607
  }
@@ -633,6 +627,7 @@ function useForm(options) {
633
627
  setErrors,
634
628
  clearErrors,
635
629
  clearError,
630
+ hasDirty,
636
631
  submitWithOptions: submit,
637
632
  submit: () => submit(),
638
633
  hasErrors: vue.computed(() => Object.values(errors.value).length > 0),
@@ -714,131 +709,8 @@ function defineLayoutProperties(properties) {
714
709
  state.setViewLayoutProperties(properties);
715
710
  }
716
711
 
717
- class Route {
718
- constructor(name, absolute) {
719
- this.name = name;
720
- this.absolute = absolute;
721
- this.definition = Route.getDefinition(name);
722
- }
723
- static getDefinition(name) {
724
- if (!state.routes.value) {
725
- throw new Error("Routing is not initialized. Make sure the Vite plugin is enabled and that `virtual:hybridly/router` is imported.");
726
- }
727
- const routes = state.routes.value;
728
- const route = routes?.routes?.[name];
729
- if (!route) {
730
- throw new Error(`Route ${name.toString()} does not exist.`);
731
- }
732
- return route;
733
- }
734
- get template() {
735
- const origin = !this.absolute ? "" : this.definition.domain ? `${state.routes.value?.url.match(/^\w+:\/\//)?.[0]}${this.definition.domain}${state.routes.value?.port ? `:${state.routes.value?.port}` : ""}` : state.routes.value?.url;
736
- return `${origin}/${this.definition.uri}`.replace(/\/+$/, "");
737
- }
738
- get parameterSegments() {
739
- return this.template.match(/{[^}?]+\??}/g)?.map((segment) => ({
740
- name: segment.replace(/{|\??}/g, ""),
741
- required: !/\?}$/.test(segment)
742
- })) ?? [];
743
- }
744
- matchesUrl(url) {
745
- if (!this.definition.methods.includes("GET")) {
746
- return false;
747
- }
748
- const pattern = this.template.replace(/(\/?){([^}?]*)(\??)}/g, (_, slash, segment, optional) => {
749
- const regex = `(?<${segment}>${this.definition.wheres?.[segment]?.replace(/(^\^)|(\$$)/g, "") || "[^/?]+"})`;
750
- return optional ? `(${slash}${regex})?` : `${slash}${regex}`;
751
- }).replace(/^\w+:\/\//, "");
752
- const [location, query] = url.replace(/^\w+:\/\//, "").split("?");
753
- const matches = new RegExp(`^${pattern}/?$`).exec(location);
754
- return matches ? { params: matches.groups, query: qs.parse(query) } : false;
755
- }
756
- compile(params) {
757
- const segments = this.parameterSegments;
758
- if (!segments.length) {
759
- return this.template;
760
- }
761
- return this.template.replace(/{([^}?]+)(\??)}/g, (_, segment, optional) => {
762
- if (!optional && [null, void 0].includes(params?.[segment])) {
763
- throw new Error(`Router error: [${segment}] parameter is required for route [${this.name}].`);
764
- }
765
- if (segments[segments.length - 1].name === segment && this.definition?.wheres?.[segment] === ".*") {
766
- return encodeURIComponent(params[segment] ?? "").replace(/%2F/g, "/");
767
- }
768
- if (this.definition?.wheres?.[segment] && !new RegExp(`^${optional ? `(${this.definition?.wheres?.[segment]})?` : this.definition?.wheres?.[segment]}$`).test(params[segment] ?? "")) {
769
- throw new Error(`Router error: [${segment}] parameter does not match required format [${this.definition?.wheres?.[segment]}] for route [${this.name}].`);
770
- }
771
- return encodeURIComponent(params[segment] ?? "");
772
- }).replace(/\/+$/, "");
773
- }
774
- }
775
-
776
- class Router extends String {
777
- constructor(name, parameters, absolute = true) {
778
- super();
779
- this.route = new Route(name, absolute);
780
- this.setParameters(parameters);
781
- }
782
- toString() {
783
- const unhandled = Object.keys(this.parameters).filter((key) => !this.route.parameterSegments.some(({ name }) => name === key)).filter((key) => key !== "_query").reduce((result, current) => ({ ...result, [current]: this.parameters[current] }), {});
784
- return this.route.compile(this.parameters) + qs.stringify({ ...unhandled, ...this.parameters._query }, {
785
- addQueryPrefix: true,
786
- arrayFormat: "indices",
787
- encodeValuesOnly: true,
788
- skipNulls: true,
789
- encoder: (value, encoder) => typeof value === "boolean" ? Number(value).toString() : encoder(value)
790
- });
791
- }
792
- static has(name) {
793
- try {
794
- Route.getDefinition(name);
795
- return true;
796
- } catch {
797
- return false;
798
- }
799
- }
800
- setParameters(parameters) {
801
- this.parameters = parameters ?? {};
802
- this.parameters = ["string", "number"].includes(typeof this.parameters) ? [this.parameters] : this.parameters;
803
- const segments = this.route.parameterSegments.filter(({ name }) => !state.routes.value?.defaults[name]);
804
- if (Array.isArray(this.parameters)) {
805
- this.parameters = this.parameters.reduce((result, current, i) => segments[i] ? { ...result, [segments[i].name]: current } : typeof current === "object" ? { ...result, ...current } : { ...result, [current]: "" }, {});
806
- } else if (segments.length === 1 && !this.parameters[segments[0].name] && (Reflect.has(this.parameters, Object.values(this.route.definition.bindings)[0]) || Reflect.has(this.parameters, "id"))) {
807
- this.parameters = { [segments[0].name]: this.parameters };
808
- }
809
- this.parameters = {
810
- ...this.getDefaults(),
811
- ...this.substituteBindings()
812
- };
813
- }
814
- getDefaults() {
815
- return this.route.parameterSegments.filter(({ name }) => state.routes.value?.defaults[name]).reduce((result, { name }) => ({ ...result, [name]: state.routes.value?.defaults[name] }), {});
816
- }
817
- substituteBindings() {
818
- return Object.entries(this.parameters).reduce((result, [key, value]) => {
819
- if (!value || typeof value !== "object" || Array.isArray(value) || !this.route.parameterSegments.some(({ name }) => name === key)) {
820
- return { ...result, [key]: value };
821
- }
822
- if (!Reflect.has(value, this.route.definition.bindings[key])) {
823
- if (Reflect.has(value, "id")) {
824
- this.route.definition.bindings[key] = "id";
825
- } else {
826
- throw new Error(`Router error: object passed as [${key}] parameter is missing route model binding key [${this.route.definition.bindings?.[key]}].`);
827
- }
828
- }
829
- return { ...result, [key]: value[this.route.definition.bindings[key]] };
830
- }, {});
831
- }
832
- valueOf() {
833
- return this.toString();
834
- }
835
- }
836
-
837
- function route(name, parameters, absolute) {
838
- return new Router(name, parameters, absolute).toString();
839
- }
840
-
841
712
  exports.can = core.can;
713
+ exports.route = core.route;
842
714
  exports.router = core.router;
843
715
  exports.HybridlyImports = HybridlyImports;
844
716
  exports.HybridlyResolver = HybridlyResolver;
@@ -847,7 +719,6 @@ exports.defineLayout = defineLayout;
847
719
  exports.defineLayoutProperties = defineLayoutProperties;
848
720
  exports.initializeHybridly = initializeHybridly;
849
721
  exports.resolvePageComponent = resolvePageComponent;
850
- exports.route = route;
851
722
  exports.useBackForward = useBackForward;
852
723
  exports.useContext = useContext;
853
724
  exports.useForm = useForm;
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as vue from 'vue';
2
2
  import { App, Plugin as Plugin$2, h, PropType, ComputedRef, DeepReadonly } from 'vue';
3
3
  import * as _hybridly_core from '@hybridly/core';
4
4
  import { HybridPayload as HybridPayload$1, ResolveComponent as ResolveComponent$1, RouterContextOptions, Plugin as Plugin$1, RouterContext, Method as Method$1, HybridRequestOptions as HybridRequestOptions$1, UrlResolvable as UrlResolvable$1 } from '@hybridly/core';
5
- export { can, router } from '@hybridly/core';
5
+ export { can, route, router } from '@hybridly/core';
6
6
  import { ProgressOptions } from '@hybridly/progress-plugin';
7
7
  import { Axios, AxiosResponse, AxiosProgressEvent } from 'axios';
8
8
  import * as _vue_shared from '@vue/shared';
@@ -28,6 +28,8 @@ interface HybridlyOptions {
28
28
  serializer?: RouterContextOptions['serializer'];
29
29
  /** Clean up the host element's payload dataset after loading. */
30
30
  cleanup?: boolean;
31
+ /** Whether to set up the devtools plugin. */
32
+ devtools?: boolean;
31
33
  /** Progressbar options. */
32
34
  progress?: boolean | Partial<ProgressOptions>;
33
35
  /** Sets up the hybridly router. */
@@ -65,7 +67,7 @@ declare const RouterLink: vue.DefineComponent<{
65
67
  default: string;
66
68
  };
67
69
  method: {
68
- type: PropType<"get" | "delete" | "post" | "put" | "patch" | Method$1>;
70
+ type: PropType<Method$1 | "get" | "post" | "put" | "patch" | "delete">;
69
71
  default: string;
70
72
  };
71
73
  data: {
@@ -95,7 +97,7 @@ declare const RouterLink: vue.DefineComponent<{
95
97
  default: string;
96
98
  };
97
99
  method: {
98
- type: PropType<"get" | "delete" | "post" | "put" | "patch" | Method$1>;
100
+ type: PropType<Method$1 | "get" | "post" | "put" | "patch" | "delete">;
99
101
  default: string;
100
102
  };
101
103
  data: {
@@ -127,7 +129,7 @@ declare const RouterLink: vue.DefineComponent<{
127
129
  default: string;
128
130
  };
129
131
  method: {
130
- type: PropType<"get" | "delete" | "post" | "put" | "patch" | Method$1>;
132
+ type: PropType<Method$1 | "get" | "post" | "put" | "patch" | "delete">;
131
133
  default: string;
132
134
  };
133
135
  data: {
@@ -149,7 +151,7 @@ declare const RouterLink: vue.DefineComponent<{
149
151
  }>>, {
150
152
  data: RequestData;
151
153
  href: string;
152
- method: "get" | "delete" | "post" | "put" | "patch" | Method$1;
154
+ method: Method$1 | "get" | "post" | "put" | "patch" | "delete";
153
155
  options: Omit<HybridRequestOptions$1, "data" | "url" | "method">;
154
156
  as: string | Record<string, any>;
155
157
  external: boolean;
@@ -254,6 +256,20 @@ interface Hooks {
254
256
  navigated: (options: NavigationOptions, context: InternalRouterContext) => MaybePromise<void>;
255
257
  }
256
258
 
259
+ interface RoutingConfiguration {
260
+ url: string;
261
+ port?: number;
262
+ defaults: Record<string, any>;
263
+ routes: Record<string, RouteDefinition>;
264
+ }
265
+ interface RouteDefinition {
266
+ uri: string;
267
+ method: Method[];
268
+ bindings: Record<string, string>;
269
+ domain?: string;
270
+ wheres?: Record<string, string>;
271
+ }
272
+
257
273
  type UrlResolvable = string | URL | Location;
258
274
  type UrlTransformable = Partial<Omit<URL, 'searchParams' | 'toJSON' | 'toString'>> & {
259
275
  query?: any;
@@ -300,7 +316,7 @@ interface HybridRequestOptions extends Omit<NavigationOptions, 'payload'> {
300
316
  /** The URL to navigation. */
301
317
  url?: UrlResolvable;
302
318
  /** HTTP verb to use for the request. */
303
- method?: Method;
319
+ method?: Method | Lowercase<Method>;
304
320
  /** Body of the request. */
305
321
  data?: RequestData;
306
322
  /** Which properties to update for this navigation. Other properties will be ignored. */
@@ -395,7 +411,7 @@ interface InternalRouterContext {
395
411
  /** The current local asset version. */
396
412
  version: string;
397
413
  /** The current adapter's functions. */
398
- adapter: Adapter;
414
+ adapter: ResolvedAdapter;
399
415
  /** Scroll positions of the current page's DOM elements. */
400
416
  scrollRegions: ScrollRegion[];
401
417
  /** Arbitrary state. */
@@ -410,6 +426,8 @@ interface InternalRouterContext {
410
426
  hooks: Partial<Record<keyof Hooks, Array<Function>>>;
411
427
  /** The Axios instance. */
412
428
  axios: Axios;
429
+ /** Routing configuration. */
430
+ routing?: RoutingConfiguration;
413
431
  }
414
432
  /** Adapter-specific functions. */
415
433
  interface Adapter {
@@ -422,6 +440,9 @@ interface Adapter {
422
440
  /** Called when the context is updated. */
423
441
  update?: (context: InternalRouterContext) => void;
424
442
  }
443
+ interface ResolvedAdapter extends Adapter {
444
+ updateRoutingConfiguration: (routing?: RoutingConfiguration) => void;
445
+ }
425
446
  interface ScrollRegion {
426
447
  top: number;
427
448
  left: number;
@@ -451,6 +472,7 @@ declare function useForm<T extends Fields = Fields>(options: FormOptions<T>): {
451
472
  setErrors: (incoming: Record<string, string>) => void;
452
473
  clearErrors: (...keys: (keyof T)[]) => void;
453
474
  clearError: (key: keyof T) => void;
475
+ hasDirty: (...keys: (keyof T)[]) => boolean;
454
476
  submitWithOptions: (optionsOverrides?: Omit<HybridRequestOptions$1, 'data'>) => Promise<_hybridly_core.NavigationResponse>;
455
477
  submit: () => Promise<_hybridly_core.NavigationResponse>;
456
478
  hasErrors: boolean;
@@ -616,29 +638,4 @@ declare function defineLayout(layouts: Layout[]): void;
616
638
  */
617
639
  declare function defineLayoutProperties<T extends Record<string, K>, K = any>(properties: T): void;
618
640
 
619
- interface RouterConfiguration {
620
- url: string;
621
- port?: number;
622
- defaults: Record<string, any>;
623
- }
624
- interface RouteDefinition {
625
- uri: string;
626
- methods: Method$1[];
627
- bindings: Record<string, string>;
628
- domain?: string;
629
- wheres?: Record<string, string>;
630
- }
631
- interface RouteCollection extends RouterConfiguration {
632
- routes: Record<string, RouteDefinition>;
633
- }
634
- interface GlobalRouteCollection extends RouteCollection {
635
- }
636
- type RouteName = keyof GlobalRouteCollection['routes'];
637
- type RouteParameters<T extends RouteName> = Record<keyof GlobalRouteCollection['routes'][T]['bindings'], any> & Record<string, any>;
638
-
639
- /**
640
- * Generates a route from the given route name.
641
- */
642
- declare function route<T extends RouteName>(name: T, parameters?: RouteParameters<T>, absolute?: boolean): string;
643
-
644
- export { AutoImportResolverOptions, GlobalRouteCollection, HybridlyImports, HybridlyResolver, Layout, RouteCollection, RouteDefinition, RouteName, RouteParameters, RouterConfiguration, RouterLink, defineLayout, defineLayoutProperties, initializeHybridly, resolvePageComponent, route, useBackForward, useContext, useForm, useHistoryState, usePaginator, useProperties, useProperty, useTypedProperty };
641
+ export { AutoImportResolverOptions, HybridlyImports, HybridlyResolver, Layout, RouterLink, defineLayout, defineLayoutProperties, initializeHybridly, resolvePageComponent, useBackForward, useContext, useForm, useHistoryState, usePaginator, useProperties, useProperty, useTypedProperty };
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { ref, shallowRef, unref, triggerRef, defineComponent, h, createApp, isRef, reactive, readonly, computed, watch, toRaw } from 'vue';
1
+ import { ref, shallowRef, unref, triggerRef, defineComponent, watch, toRaw, h, createApp, isRef, reactive, readonly, computed } from 'vue';
2
2
  import { registerHook, createRouter, makeUrl, router } from '@hybridly/core';
3
- export { can, router } from '@hybridly/core';
3
+ export { can, route, router } from '@hybridly/core';
4
4
  import { debug, showPageComponentErrorModal, merge, clone } from '@hybridly/utils';
5
5
  import { progress } from '@hybridly/progress-plugin';
6
6
  import { setupDevtoolsPlugin } from '@vue/devtools-api';
7
- import qs, { parse, stringify } from 'qs';
7
+ import qs from 'qs';
8
8
  import isEqual from 'lodash.isequal';
9
9
 
10
10
  const state = {
@@ -15,13 +15,6 @@ const state = {
15
15
  viewKey: ref(),
16
16
  dialog: shallowRef(),
17
17
  dialogKey: ref(),
18
- routes: ref(),
19
- setRoutes(routes) {
20
- debug.adapter("vue:state:routes", "Setting routes:", routes);
21
- if (routes) {
22
- state.routes.value = unref(routes);
23
- }
24
- },
25
18
  setView(view) {
26
19
  debug.adapter("vue:state:view", "Setting view:", view);
27
20
  state.view.value = view;
@@ -55,13 +48,12 @@ const state = {
55
48
 
56
49
  const wrapper = defineComponent({
57
50
  name: "Hybridly",
58
- setup(props) {
59
- if (typeof window !== "undefined") {
60
- state.setContext(props.context);
61
- if (!props.context) {
62
- throw new Error("Hybridly was not properly initialized. The context is missing.");
63
- }
64
- }
51
+ setup() {
52
+ const properties = ref();
53
+ watch(() => state.context.value?.view?.properties, (value) => {
54
+ debug.adapter("vue:properties", "Updating properties.", toRaw(value));
55
+ properties.value = value;
56
+ }, { immediate: true });
65
57
  function renderLayout(child) {
66
58
  debug.adapter("vue:render:layout", "Rendering layout.");
67
59
  if (typeof state.view.value?.layout === "function") {
@@ -72,14 +64,14 @@ const wrapper = defineComponent({
72
64
  layout.inheritAttrs = !!layout.inheritAttrs;
73
65
  return h(layout, {
74
66
  ...state.view.value?.layoutProperties ?? {},
75
- ...state.context.value.view.properties
67
+ ...properties.value
76
68
  }, () => child2);
77
69
  });
78
70
  }
79
71
  return [
80
72
  h(state.view.value?.layout, {
81
73
  ...state.view.value?.layoutProperties ?? {},
82
- ...state.context.value.view.properties
74
+ ...properties.value
83
75
  }, () => child),
84
76
  renderDialog()
85
77
  ];
@@ -88,7 +80,7 @@ const wrapper = defineComponent({
88
80
  debug.adapter("vue:render:view", "Rendering view.");
89
81
  state.view.value.inheritAttrs = !!state.view.value.inheritAttrs;
90
82
  return h(state.view.value, {
91
- ...state.context.value.view.properties,
83
+ ...properties.value,
92
84
  key: state.viewKey.value
93
85
  });
94
86
  }
@@ -101,29 +93,24 @@ const wrapper = defineComponent({
101
93
  });
102
94
  }
103
95
  }
104
- return () => {
105
- if (state.view.value) {
106
- const view = renderView();
107
- if (state.viewLayout.value) {
108
- state.view.value.layout = state.viewLayout.value;
109
- state.viewLayout.value = void 0;
110
- }
111
- if (state.viewLayoutProperties.value) {
112
- state.view.value.layoutProperties = state.viewLayoutProperties.value;
113
- state.viewLayoutProperties.value = void 0;
114
- }
115
- if (state.view.value.layout) {
116
- return renderLayout(view);
117
- }
118
- return [view, renderDialog()];
96
+ return (...a) => {
97
+ if (!state.view.value) {
98
+ return;
99
+ }
100
+ debug.adapter("vue:render:wrapper", "Rendering wrapper component.", a.map(toRaw));
101
+ const view = renderView();
102
+ if (state.viewLayout.value) {
103
+ state.view.value.layout = state.viewLayout.value;
119
104
  }
105
+ if (state.viewLayoutProperties.value) {
106
+ state.view.value.layoutProperties = state.viewLayoutProperties.value;
107
+ state.viewLayoutProperties.value = void 0;
108
+ }
109
+ if (state.view.value.layout) {
110
+ return renderLayout(view);
111
+ }
112
+ return [view, renderDialog()];
120
113
  };
121
- },
122
- props: {
123
- context: {
124
- type: Object,
125
- required: true
126
- }
127
114
  }
128
115
  });
129
116
 
@@ -165,8 +152,8 @@ function setupDevtools(app) {
165
152
  });
166
153
  payload.instanceData.state.push({
167
154
  type: hybridlyStateType,
168
- key: "router",
169
- value: state.routes.value
155
+ key: "routing",
156
+ value: state.context.value?.routing
170
157
  });
171
158
  });
172
159
  api.on.editComponentState((payload) => {
@@ -223,7 +210,7 @@ function setupDevtools(app) {
223
210
  });
224
211
  });
225
212
  }
226
- const plugin = {
213
+ const devtools = {
227
214
  install(app) {
228
215
  if (process.env.NODE_ENV === "development" || __VUE_PROD_DEVTOOLS__) {
229
216
  setupDevtools(app);
@@ -259,18 +246,26 @@ async function initializeHybridly(options) {
259
246
  },
260
247
  payload
261
248
  }));
262
- const render = () => h(wrapper, { context: state.context.value });
249
+ if (typeof window !== "undefined") {
250
+ window.addEventListener("hybridly:routing", (event) => {
251
+ state.context.value?.adapter.updateRoutingConfiguration(event.detail);
252
+ });
253
+ window.dispatchEvent(new CustomEvent("hybridly:routing", { detail: window?.hybridly?.routing }));
254
+ }
255
+ const render = () => h(wrapper);
263
256
  if (options.setup) {
264
257
  return await options.setup({
265
258
  element,
266
259
  wrapper,
267
260
  render,
268
- hybridly: plugin,
261
+ hybridly: devtools,
269
262
  props: { context: state.context.value }
270
263
  });
271
264
  }
272
265
  const app = createApp({ render });
273
- app.use(plugin);
266
+ if (options.devtools !== false) {
267
+ app.use(devtools);
268
+ }
274
269
  await options.enhanceVue?.(app);
275
270
  return app.mount(element);
276
271
  }
@@ -296,14 +291,6 @@ function prepare(options) {
296
291
  }
297
292
  throw new Error("Either `initializeHybridly#resolve` or `initializeHybridly#pages` should be defined.");
298
293
  };
299
- if (typeof window !== "undefined") {
300
- state.setRoutes(window?.hybridly?.routes);
301
- window.addEventListener("hybridly:routes", (event) => {
302
- if (event.detail) {
303
- state.setRoutes(event.detail);
304
- }
305
- });
306
- }
307
294
  if (options.progress !== false) {
308
295
  options.plugins = [
309
296
  progress(typeof options.progress === "object" ? options.progress : {}),
@@ -431,12 +418,12 @@ const HybridlyImports = {
431
418
  "useHistoryState",
432
419
  "usePaginator",
433
420
  "defineLayout",
434
- "defineLayoutProperties",
435
- "route"
421
+ "defineLayoutProperties"
436
422
  ],
437
423
  "hybridly": [
438
424
  "registerHook",
439
425
  "router",
426
+ "route",
440
427
  "can"
441
428
  ]
442
429
  };
@@ -513,7 +500,7 @@ function safeClone(obj) {
513
500
  return clone(toRaw(obj));
514
501
  }
515
502
  function useForm(options) {
516
- const shouldRemember = options?.key !== false;
503
+ const shouldRemember = !!options?.key;
517
504
  const historyKey = options?.key ?? "form:default";
518
505
  const historyData = shouldRemember ? router.history.get(historyKey) : void 0;
519
506
  const timeoutIds = {
@@ -543,13 +530,14 @@ function useForm(options) {
543
530
  function submit(optionsOverrides) {
544
531
  const url = typeof options.url === "function" ? options.url() : options.url;
545
532
  const data = typeof options.transform === "function" ? options.transform?.(fields) : fields;
533
+ const preserveState = optionsOverrides?.preserveState ?? options.preserveState;
546
534
  return router.navigate({
547
535
  ...options,
548
536
  url: url ?? state.context.value?.url,
549
537
  method: options.method ?? "POST",
550
538
  ...optionsOverrides,
551
539
  data: safeClone(data),
552
- preserveState: optionsOverrides?.preserveState === void 0 && options.method !== "GET" ? true : optionsOverrides?.preserveState,
540
+ preserveState: preserveState === void 0 && options.method !== "GET" ? true : preserveState,
553
541
  hooks: {
554
542
  before: (navigation, context) => {
555
543
  failed.value = false;
@@ -557,7 +545,6 @@ function useForm(options) {
557
545
  recentlySuccessful.value = false;
558
546
  clearTimeout(timeoutIds.recentlySuccessful);
559
547
  clearTimeout(timeoutIds.recentlyFailed);
560
- clearErrors();
561
548
  return options.hooks?.before?.(navigation, context);
562
549
  },
563
550
  start: (context) => {
@@ -576,6 +563,7 @@ function useForm(options) {
576
563
  return options.hooks?.error?.(incoming, context);
577
564
  },
578
565
  success: (payload, context) => {
566
+ clearErrors();
579
567
  if (options?.reset !== false) {
580
568
  reset();
581
569
  }
@@ -600,6 +588,12 @@ function useForm(options) {
600
588
  clearError(key);
601
589
  });
602
590
  }
591
+ function hasDirty(...keys) {
592
+ if (keys.length === 0) {
593
+ return isDirty.value;
594
+ }
595
+ return keys.some((key) => !isEqual(toRaw(fields[key]), toRaw(initial[key])));
596
+ }
603
597
  function clearError(key) {
604
598
  errors.value[key] = void 0;
605
599
  }
@@ -625,6 +619,7 @@ function useForm(options) {
625
619
  setErrors,
626
620
  clearErrors,
627
621
  clearError,
622
+ hasDirty,
628
623
  submitWithOptions: submit,
629
624
  submit: () => submit(),
630
625
  hasErrors: computed(() => Object.values(errors.value).length > 0),
@@ -706,128 +701,4 @@ function defineLayoutProperties(properties) {
706
701
  state.setViewLayoutProperties(properties);
707
702
  }
708
703
 
709
- class Route {
710
- constructor(name, absolute) {
711
- this.name = name;
712
- this.absolute = absolute;
713
- this.definition = Route.getDefinition(name);
714
- }
715
- static getDefinition(name) {
716
- if (!state.routes.value) {
717
- throw new Error("Routing is not initialized. Make sure the Vite plugin is enabled and that `virtual:hybridly/router` is imported.");
718
- }
719
- const routes = state.routes.value;
720
- const route = routes?.routes?.[name];
721
- if (!route) {
722
- throw new Error(`Route ${name.toString()} does not exist.`);
723
- }
724
- return route;
725
- }
726
- get template() {
727
- const origin = !this.absolute ? "" : this.definition.domain ? `${state.routes.value?.url.match(/^\w+:\/\//)?.[0]}${this.definition.domain}${state.routes.value?.port ? `:${state.routes.value?.port}` : ""}` : state.routes.value?.url;
728
- return `${origin}/${this.definition.uri}`.replace(/\/+$/, "");
729
- }
730
- get parameterSegments() {
731
- return this.template.match(/{[^}?]+\??}/g)?.map((segment) => ({
732
- name: segment.replace(/{|\??}/g, ""),
733
- required: !/\?}$/.test(segment)
734
- })) ?? [];
735
- }
736
- matchesUrl(url) {
737
- if (!this.definition.methods.includes("GET")) {
738
- return false;
739
- }
740
- const pattern = this.template.replace(/(\/?){([^}?]*)(\??)}/g, (_, slash, segment, optional) => {
741
- const regex = `(?<${segment}>${this.definition.wheres?.[segment]?.replace(/(^\^)|(\$$)/g, "") || "[^/?]+"})`;
742
- return optional ? `(${slash}${regex})?` : `${slash}${regex}`;
743
- }).replace(/^\w+:\/\//, "");
744
- const [location, query] = url.replace(/^\w+:\/\//, "").split("?");
745
- const matches = new RegExp(`^${pattern}/?$`).exec(location);
746
- return matches ? { params: matches.groups, query: parse(query) } : false;
747
- }
748
- compile(params) {
749
- const segments = this.parameterSegments;
750
- if (!segments.length) {
751
- return this.template;
752
- }
753
- return this.template.replace(/{([^}?]+)(\??)}/g, (_, segment, optional) => {
754
- if (!optional && [null, void 0].includes(params?.[segment])) {
755
- throw new Error(`Router error: [${segment}] parameter is required for route [${this.name}].`);
756
- }
757
- if (segments[segments.length - 1].name === segment && this.definition?.wheres?.[segment] === ".*") {
758
- return encodeURIComponent(params[segment] ?? "").replace(/%2F/g, "/");
759
- }
760
- if (this.definition?.wheres?.[segment] && !new RegExp(`^${optional ? `(${this.definition?.wheres?.[segment]})?` : this.definition?.wheres?.[segment]}$`).test(params[segment] ?? "")) {
761
- throw new Error(`Router error: [${segment}] parameter does not match required format [${this.definition?.wheres?.[segment]}] for route [${this.name}].`);
762
- }
763
- return encodeURIComponent(params[segment] ?? "");
764
- }).replace(/\/+$/, "");
765
- }
766
- }
767
-
768
- class Router extends String {
769
- constructor(name, parameters, absolute = true) {
770
- super();
771
- this.route = new Route(name, absolute);
772
- this.setParameters(parameters);
773
- }
774
- toString() {
775
- const unhandled = Object.keys(this.parameters).filter((key) => !this.route.parameterSegments.some(({ name }) => name === key)).filter((key) => key !== "_query").reduce((result, current) => ({ ...result, [current]: this.parameters[current] }), {});
776
- return this.route.compile(this.parameters) + stringify({ ...unhandled, ...this.parameters._query }, {
777
- addQueryPrefix: true,
778
- arrayFormat: "indices",
779
- encodeValuesOnly: true,
780
- skipNulls: true,
781
- encoder: (value, encoder) => typeof value === "boolean" ? Number(value).toString() : encoder(value)
782
- });
783
- }
784
- static has(name) {
785
- try {
786
- Route.getDefinition(name);
787
- return true;
788
- } catch {
789
- return false;
790
- }
791
- }
792
- setParameters(parameters) {
793
- this.parameters = parameters ?? {};
794
- this.parameters = ["string", "number"].includes(typeof this.parameters) ? [this.parameters] : this.parameters;
795
- const segments = this.route.parameterSegments.filter(({ name }) => !state.routes.value?.defaults[name]);
796
- if (Array.isArray(this.parameters)) {
797
- this.parameters = this.parameters.reduce((result, current, i) => segments[i] ? { ...result, [segments[i].name]: current } : typeof current === "object" ? { ...result, ...current } : { ...result, [current]: "" }, {});
798
- } else if (segments.length === 1 && !this.parameters[segments[0].name] && (Reflect.has(this.parameters, Object.values(this.route.definition.bindings)[0]) || Reflect.has(this.parameters, "id"))) {
799
- this.parameters = { [segments[0].name]: this.parameters };
800
- }
801
- this.parameters = {
802
- ...this.getDefaults(),
803
- ...this.substituteBindings()
804
- };
805
- }
806
- getDefaults() {
807
- return this.route.parameterSegments.filter(({ name }) => state.routes.value?.defaults[name]).reduce((result, { name }) => ({ ...result, [name]: state.routes.value?.defaults[name] }), {});
808
- }
809
- substituteBindings() {
810
- return Object.entries(this.parameters).reduce((result, [key, value]) => {
811
- if (!value || typeof value !== "object" || Array.isArray(value) || !this.route.parameterSegments.some(({ name }) => name === key)) {
812
- return { ...result, [key]: value };
813
- }
814
- if (!Reflect.has(value, this.route.definition.bindings[key])) {
815
- if (Reflect.has(value, "id")) {
816
- this.route.definition.bindings[key] = "id";
817
- } else {
818
- throw new Error(`Router error: object passed as [${key}] parameter is missing route model binding key [${this.route.definition.bindings?.[key]}].`);
819
- }
820
- }
821
- return { ...result, [key]: value[this.route.definition.bindings[key]] };
822
- }, {});
823
- }
824
- valueOf() {
825
- return this.toString();
826
- }
827
- }
828
-
829
- function route(name, parameters, absolute) {
830
- return new Router(name, parameters, absolute).toString();
831
- }
832
-
833
- export { HybridlyImports, HybridlyResolver, RouterLink, defineLayout, defineLayoutProperties, initializeHybridly, resolvePageComponent, route, useBackForward, useContext, useForm, useHistoryState, usePaginator, useProperties, useProperty, useTypedProperty };
704
+ export { HybridlyImports, HybridlyResolver, RouterLink, defineLayout, defineLayoutProperties, initializeHybridly, resolvePageComponent, useBackForward, useContext, useForm, useHistoryState, usePaginator, useProperties, useProperty, useTypedProperty };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybridly/vue",
3
- "version": "0.0.1-alpha.13",
3
+ "version": "0.0.1-alpha.15",
4
4
  "description": "A solution to develop server-driven, client-rendered applications",
5
5
  "keywords": [
6
6
  "hybridly",
@@ -38,14 +38,14 @@
38
38
  "vue": "^3.2.37"
39
39
  },
40
40
  "dependencies": {
41
- "@hybridly/core": "0.0.1-alpha.13",
42
- "@hybridly/progress-plugin": "0.0.1-alpha.13",
43
- "@hybridly/utils": "0.0.1-alpha.13",
44
41
  "@vue/devtools-api": "^6.4.5",
45
42
  "defu": "^6.1.1",
46
43
  "lodash.isequal": "^4.5.0",
47
44
  "nprogress": "^0.2.0",
48
- "qs": "^6.11.0"
45
+ "qs": "^6.11.0",
46
+ "@hybridly/core": "0.0.1-alpha.15",
47
+ "@hybridly/progress-plugin": "0.0.1-alpha.15",
48
+ "@hybridly/utils": "0.0.1-alpha.15"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/lodash": "^4.14.190",