@@ -2,8 +2,11 @@ import Base from '../core/Base.mjs';
22import HashHistory from '../util/HashHistory.mjs' ;
33
44const
5- amountSlashesRegex = / \/ / g,
6- routeParamRegex = / { [ ^ \s / ] + } / g
5+ amountSlashesRegex = / \/ / g,
6+ // Regex to match route parameters like {paramName}, {*paramName}, or {...paramName}
7+ routeParamRegex = / { ( \* | \. \. \. ) ? ( [ ^ } ] + ) } / g,
8+ // Regex to extract the parameter name from a single route segment (e.g., {*itemId} -> itemId)
9+ paramNameExtractionRegex = / { ( \* | \. \. \. ) ? ( [ ^ } ] + ) } / ;
710
811/**
912 * @class Neo.controller.Base
@@ -22,25 +25,33 @@ class Controller extends Base {
2225 */
2326 ntype : 'controller' ,
2427 /**
25- * If the URL does not contain a hash value when creating this controller instance,
26- * neo will set this hash value for us .
28+ * If the URL does not contain a hash value when this controller instance is created ,
29+ * Neo.mjs will automatically set this hash value, ensuring a default route is active .
2730 * @member {String|null} defaultHash=null
2831 */
2932 defaultHash : null ,
3033 /**
34+ * Specifies the handler method to be invoked when no other defined route matches the URL hash.
35+ * This acts as a fallback for unhandled routes.
3136 * @member {String|null} defaultRoute=null
3237 */
3338 defaultRoute : null ,
3439 /**
40+ * Internal map of compiled regular expressions for each route, used for efficient hash matching.
41+ * @protected
3542 * @member {Object} handleRoutes={}
3643 */
3744 handleRoutes : { } ,
3845 /**
46+ * Defines the routing rules for the controller. Keys are route patterns, and values are either
47+ * handler method names (String) or objects containing `handler` and optional `preHandler` method names.
48+ * Route patterns can include parameters like `{paramName}` and wildcards like `{*paramName}` for nested paths.
3949 * @example
4050 * routes: {
4151 * '/home' : 'handleHomeRoute',
4252 * '/users/{userId}' : {handler: 'handleUserRoute', preHandler: 'preHandleUserRoute'},
4353 * '/users/{userId}/posts/{postId}': 'handlePostRoute',
54+ * '/learn/{*itemId}' : 'onLearnRoute', // Captures nested paths like /learn/gettingstarted/Workspaces
4455 * 'default' : 'handleOtherRoutes'
4556 * }
4657 * @member {Object} routes_={}
@@ -49,6 +60,8 @@ class Controller extends Base {
4960 }
5061
5162 /**
63+ * Creates a new Controller instance and registers its `onHashChange` method
64+ * to listen for changes in the browser's URL hash.
5265 * @param {Object } config
5366 */
5467 construct ( config ) {
@@ -58,7 +71,8 @@ class Controller extends Base {
5871 }
5972
6073 /**
61- * Triggered after the routes config got changed
74+ * Processes the defined routes configuration, compiling route patterns into regular expressions
75+ * for efficient matching and sorting them by specificity (more slashes first).
6276 * @param {Object } value
6377 * @param {Object } oldValue
6478 * @protected
@@ -78,7 +92,13 @@ class Controller extends Base {
7892 if ( key . toLowerCase ( ) === 'default' ) {
7993 me . defaultRoute = value [ key ]
8094 } else {
81- me . handleRoutes [ key ] = new RegExp ( key . replace ( routeParamRegex , '([\\w-.]+)' ) + '$' )
95+ me . handleRoutes [ key ] = new RegExp ( key . replace ( routeParamRegex , ( match , isWildcard , paramName ) => {
96+ if ( isWildcard || paramName . startsWith ( '*' ) ) {
97+ return '(.*)' ;
98+ } else {
99+ return '([\\w-.]+)' ;
100+ }
101+ } ) )
82102 }
83103 } )
84104 }
@@ -93,7 +113,8 @@ class Controller extends Base {
93113 }
94114
95115 /**
96- *
116+ * Lifecycle method called after the controller has been constructed.
117+ * It handles the initial routing based on the current URL hash or `defaultHash`.
97118 */
98119 async onConstructed ( ) {
99120 let me = this ,
@@ -118,9 +139,10 @@ class Controller extends Base {
118139 }
119140
120141 /**
121- * Placeholder method which gets triggered when the hash inside the browser url changes
122- * @param {Object } value
123- * @param {Object } oldValue
142+ * Handles changes in the browser's URL hash. It identifies the most specific matching route
143+ * and dispatches the corresponding handler, optionally executing a preHandler first.
144+ * @param {Object } value - The new hash history entry.
145+ * @param {Object } oldValue - The previous hash history entry.
124146 */
125147 async onHashChange ( value , oldValue ) {
126148 // We only want to trigger hash changes for the same browser window (SharedWorker context)
@@ -129,85 +151,88 @@ class Controller extends Base {
129151 }
130152
131153 let me = this ,
132- counter = 0 ,
133- hasRouteBeenFound = false ,
134154 { handleRoutes, routes} = me ,
135155 routeKeys = Object . keys ( handleRoutes ) ,
136- routeKeysLength = routeKeys . length ,
137- arrayParamIds , arrayParamValues , handler , key , paramObject , preHandler , responsePreHandler , result , route ;
156+ bestMatch = null ,
157+ bestMatchKey = null ,
158+ bestMatchParams = null ;
138159
139- while ( routeKeysLength > 0 && counter < routeKeysLength && ! hasRouteBeenFound ) {
140- key = routeKeys [ counter ] ;
141- handler = null ;
142- preHandler = null ;
143- responsePreHandler = null ;
144- paramObject = { } ;
145- result = value . hashString . match ( handleRoutes [ key ] ) ;
160+ for ( let i = 0 ; i < routeKeys . length ; i ++ ) {
161+ const key = routeKeys [ i ] ;
162+ const result = value . hashString . match ( handleRoutes [ key ] ) ;
146163
147164 if ( result ) {
148- arrayParamIds = key . match ( routeParamRegex ) ;
149- arrayParamValues = result . splice ( 1 , result . length - 1 ) ;
150-
151- if ( arrayParamIds && arrayParamIds . length !== arrayParamValues . length ) {
152- throw 'Number of IDs and number of Values do not match'
165+ const arrayParamIds = key . match ( routeParamRegex ) ;
166+ const arrayParamValues = result . splice ( 1 , result . length - 1 ) ;
167+ const paramObject = { } ;
168+
169+ if ( arrayParamIds ) {
170+ for ( let j = 0 ; j < arrayParamIds . length ; j ++ ) {
171+ const paramMatch = arrayParamIds [ j ] . match ( paramNameExtractionRegex ) ;
172+ if ( paramMatch ) {
173+ const paramName = paramMatch [ 2 ] ;
174+ paramObject [ paramName ] = arrayParamValues [ j ] ;
175+ }
176+ }
153177 }
154178
155- for ( let i = 0 ; arrayParamIds && i < arrayParamIds . length ; i ++ ) {
156- paramObject [ arrayParamIds [ i ] . substring ( 1 , arrayParamIds [ i ] . length - 1 ) ] = arrayParamValues [ i ]
179+ // Logic to determine the best matching route:
180+ // 1. Prioritize routes that match a longer string (more specific match).
181+ // 2. If lengths are equal, prioritize routes with more slashes (deeper nesting).
182+ if ( ! bestMatch || ( result [ 0 ] . length > bestMatch [ 0 ] . length ) ||
183+ ( result [ 0 ] . length === bestMatch [ 0 ] . length && ( key . match ( amountSlashesRegex ) || [ ] ) . length > ( bestMatchKey . match ( amountSlashesRegex ) || [ ] ) . length ) ) {
184+ bestMatch = result ;
185+ bestMatchKey = key ;
186+ bestMatchParams = paramObject ;
157187 }
188+ }
189+ }
158190
159- route = routes [ key ] ;
160-
161- if ( Neo . isString ( route ) ) {
162- handler = route ;
163- responsePreHandler = true
164- } else if ( Neo . isObject ( route ) ) {
165- handler = route . handler ;
166- preHandler = route . preHandler
167- }
191+ if ( bestMatch ) {
192+ const route = routes [ bestMatchKey ] ;
193+ let handler = null ;
194+ let preHandler = null ;
168195
169- hasRouteBeenFound = true
196+ if ( Neo . isString ( route ) ) {
197+ handler = route ;
198+ } else if ( Neo . isObject ( route ) ) {
199+ handler = route . handler ;
200+ preHandler = route . preHandler ;
170201 }
171202
172- counter ++
173- }
174-
175- // execute
176- if ( hasRouteBeenFound ) {
203+ let responsePreHandler = true ;
177204 if ( preHandler ) {
178- responsePreHandler = await me [ preHandler ] ?. call ( me , paramObject , value , oldValue )
179- } else {
180- responsePreHandler = true
205+ responsePreHandler = await me [ preHandler ] ?. call ( me , bestMatchParams , value , oldValue ) ;
181206 }
182207
183208 if ( responsePreHandler ) {
184- await me [ handler ] ?. call ( me , paramObject , value , oldValue )
209+ await me [ handler ] ?. call ( me , bestMatchParams , value , oldValue ) ;
185210 }
186- }
187-
188- if ( routeKeys . length > 0 && ! hasRouteBeenFound ) {
211+ } else {
189212 if ( me . defaultRoute ) {
190- me [ me . defaultRoute ] ?. ( value , oldValue )
213+ me [ me . defaultRoute ] ?. ( value , oldValue ) ;
191214 } else {
192- me . onNoRouteFound ( value , oldValue )
215+ me . onNoRouteFound ( value , oldValue ) ;
193216 }
194217 }
195218 }
196219
197220 /**
198- * Placeholder method which gets triggered when an invalid route is called
199- * @param {Object } value
200- * @param {Object } oldValue
221+ * Placeholder method invoked when no matching route is found for the current URL hash.
222+ * Controllers can override this to implement custom behavior for unhandled routes.
223+ * @param {Object } value - The current hash history entry.
224+ * @param {Object } oldValue - The previous hash history entry.
201225 */
202226 onNoRouteFound ( value , oldValue ) {
203227
204228 }
205229
206230 /**
207- * Internal helper method to sort routes by their amount of slashes
208- * @param {String } route1
209- * @param {String } route2
210- * @returns {Number }
231+ * Internal helper method to sort routes by their specificity.
232+ * Routes with more slashes are considered more specific and are prioritized.
233+ * @param {String } route1 - The first route string to compare.
234+ * @param {String } route2 - The second route string to compare.
235+ * @returns {Number } A negative value if route1 is more specific, a positive value if route2 is more specific, or 0 if they have equal specificity.
211236 */
212237 #sortRoutes( route1 , route2 ) {
213238 return ( route1 . match ( amountSlashesRegex ) || [ ] ) . length - ( route2 . match ( amountSlashesRegex ) || [ ] ) . length
0 commit comments