betal-fe 4.2.0 → 4.4.0

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
@@ -15,7 +15,7 @@ npm install betal-fe
15
15
  - **Reactive State** - Automatic re-rendering on state changes
16
16
  - **Lifecycle Hooks** - `onMounted`, `onUnmounted`, `onPropsChange`, `onStateChange`
17
17
  - **Slots** - Content projection for flexible component composition (Vue-style)
18
- - **Hash Router** - Built-in SPA routing with route guards and params
18
+ - **Hash Router** - Built-in SPA routing with route guards, params, and scroll behavior
19
19
  - **Event System** - Parent-child communication via emit/subscribe
20
20
  - **Scheduler** - Async lifecycle execution with microtask batching
21
21
 
@@ -65,7 +65,9 @@ const router = new HashRouter([
65
65
  { path: '/', component: HomePage },
66
66
  { path: '/about', component: AboutPage },
67
67
  { path: '/user/:id', component: UserPage },
68
- ]);
68
+ ], {
69
+ scrollBehavior: 'top' // 'top', false, or custom function
70
+ });
69
71
 
70
72
  const App = defineComponent({
71
73
  render() {
@@ -197,10 +199,36 @@ Creates a hash-based router.
197
199
  const router = new HashRouter([
198
200
  { path: '/', component: HomePage },
199
201
  { path: '/user/:id', component: UserPage },
202
+ {
203
+ path: '/admin',
204
+ component: AdminPage,
205
+ beforeEnter: (from, to) => {
206
+ // Route guard - return false to cancel, string to redirect
207
+ return isLoggedIn() ? true : '/login';
208
+ }
209
+ }
200
210
  ], {
201
- beforeEnter: (to, from) => {
202
- // Route guard - return false to cancel navigation
203
- return true;
211
+ scrollBehavior: 'top' // 'top' (default), false, or custom function
212
+ });
213
+ ```
214
+
215
+ #### Scroll Behavior Options
216
+
217
+ ```javascript
218
+ // Default: scroll to top
219
+ new HashRouter(routes);
220
+
221
+ // Disable scroll
222
+ new HashRouter(routes, { scrollBehavior: false });
223
+
224
+ // Custom function for route-specific behavior
225
+ new HashRouter(routes, {
226
+ scrollBehavior: (from, to) => {
227
+ // Don't scroll when navigating within the same section
228
+ if (from?.path?.startsWith('/docs') && to?.path?.startsWith('/docs')) {
229
+ return null; // Prevent scrolling
230
+ }
231
+ return { x: 0, y: 0, behavior: 'smooth' };
204
232
  }
205
233
  });
206
234
  ```
@@ -216,7 +244,14 @@ this.appContext.router.navigateTo('/path')
216
244
  #### `RouterLink` Component
217
245
 
218
246
  ```javascript
219
- h(RouterLink, { to: '/about', activeClass: 'active' }, ['About'])
247
+ h(RouterLink, { to: '/about' }, ['About'])
248
+
249
+ // With anchor fragments
250
+ h(RouterLink, { to: '/about#team' }, ['Meet the Team'])
251
+ h(RouterLink, { to: '#features' }, ['Jump to Features'])
252
+
253
+ // With custom classes and attributes
254
+ h(RouterLink, { to: '/about', class: 'nav-link active' }, ['About'])
220
255
  ```
221
256
 
222
257
  #### `RouterOutlet` Component
package/dist/betal-fe.js CHANGED
@@ -530,6 +530,7 @@ class HashRouter {
530
530
  #dispatcher = new Dispatcher();
531
531
  #subscriptions = new WeakMap();
532
532
  #subscriberFns = new Set();
533
+ #scrollBehavior = 'top';
533
534
  get matchedRoute() {
534
535
  return this.#matchedRoute;
535
536
  }
@@ -549,9 +550,12 @@ class HashRouter {
549
550
  return hash.slice(1);
550
551
  }
551
552
  #onPopState = () => this.#matchCurrentRoute();
552
- constructor(routes = []) {
553
+ constructor(routes = [], options = {}) {
553
554
  assert(Array.isArray(routes), "Routes must be an array");
554
555
  this.#matchers = routes.map(makeRouteMatcher);
556
+ if (options.scrollBehavior !== undefined) {
557
+ this.#scrollBehavior = options.scrollBehavior;
558
+ }
555
559
  }
556
560
  async init() {
557
561
  if (this.#isInitialized) {
@@ -573,9 +577,12 @@ class HashRouter {
573
577
  this.#isInitialized = false;
574
578
  }
575
579
  async navigateTo(path) {
576
- const matcher = this.#matchers.find((matcher) => matcher.checkMatch(path));
580
+ const hashIndex = path.indexOf('#', 1);
581
+ const pathWithoutHash = hashIndex !== -1 ? path.slice(0, hashIndex) : path;
582
+ const hash = hashIndex !== -1 ? path.slice(hashIndex + 1) : null;
583
+ const matcher = this.#matchers.find((matcher) => matcher.checkMatch(pathWithoutHash));
577
584
  if (matcher == null) {
578
- console.warn(`[Router] No route matches path "${path}"`);
585
+ console.warn(`[Router] No route matches path "${pathWithoutHash}"`);
579
586
  this.#matchedRoute = null;
580
587
  this.#params = {};
581
588
  this.#query = {};
@@ -593,12 +600,39 @@ class HashRouter {
593
600
  }
594
601
  if (shouldNavigate) {
595
602
  this.#matchedRoute = matcher.route;
596
- this.#params = matcher.extractParams(path);
597
- this.#query = matcher.extractQuery(path);
603
+ this.#params = matcher.extractParams(pathWithoutHash);
604
+ this.#query = matcher.extractQuery(pathWithoutHash);
598
605
  this.#pushState(path);
606
+ this.#handleScrollBehavior(from, to, hash);
599
607
  this.#dispatcher.dispatch(ROUTER_EVENT, { from, to, router: this });
600
608
  }
601
609
  }
610
+ #handleScrollBehavior(from, to, hash) {
611
+ if (this.#scrollBehavior === false) {
612
+ return;
613
+ }
614
+ setTimeout(() => {
615
+ if (typeof this.#scrollBehavior === 'function') {
616
+ const position = this.#scrollBehavior(from, to, { hash });
617
+ if (position) {
618
+ window.scrollTo({
619
+ left: position.x || 0,
620
+ top: position.y || 0,
621
+ behavior: position.behavior || 'auto'
622
+ });
623
+ }
624
+ } else if (this.#scrollBehavior === 'top') {
625
+ if (hash) {
626
+ const element = document.getElementById(hash);
627
+ if (element) {
628
+ element.scrollIntoView({ behavior: 'auto' });
629
+ return;
630
+ }
631
+ }
632
+ window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
633
+ }
634
+ }, 0);
635
+ }
602
636
  back() {
603
637
  window.history.back();
604
638
  }
@@ -1121,42 +1155,18 @@ const RouterLink = defineComponent({
1121
1155
  return h(
1122
1156
  "a",
1123
1157
  {
1124
- href: to,
1158
+ href: `#${to}`,
1125
1159
  ...rest,
1126
1160
  on: {
1127
1161
  click: (e) => {
1128
1162
  e.preventDefault();
1129
- this.handleNavigation(to);
1163
+ this.appContext.router.navigateTo(to);
1130
1164
  },
1131
1165
  },
1132
1166
  },
1133
1167
  [hSlot()]
1134
1168
  );
1135
1169
  },
1136
- handleNavigation(to) {
1137
- const anchorIndex = to.indexOf('#');
1138
- if (anchorIndex !== -1 && anchorIndex > 0) {
1139
- const path = to.substring(0, anchorIndex);
1140
- const anchor = to.substring(anchorIndex + 1);
1141
- this.appContext.router.navigateTo(path);
1142
- setTimeout(() => {
1143
- this.scrollToAnchor(anchor);
1144
- }, 0);
1145
- } else if (anchorIndex === 0) {
1146
- const anchor = to.substring(1);
1147
- this.scrollToAnchor(anchor);
1148
- } else {
1149
- this.appContext.router.navigateTo(to);
1150
- }
1151
- },
1152
- scrollToAnchor(anchorId) {
1153
- const element = document.getElementById(anchorId);
1154
- if (element) {
1155
- element.scrollIntoView({ behavior: 'smooth', block: 'start' });
1156
- } else {
1157
- console.warn(`[RouterLink] Element with id "${anchorId}" not found`);
1158
- }
1159
- },
1160
1170
  });
1161
1171
  const RouterOutlet = defineComponent({
1162
1172
  state() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "betal-fe",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
4
4
  "type": "module",
5
5
  "main": "dist/betal-fe.js",
6
6
  "files": [