I have a URL like the following and I need to implement route handlers for it:
/shop/et--zubehoer-c16/zubehoer-c48/top-cases--taschen-c63/top-case-32l-rosso-passione-p64
That follows the logic:
/shop/{path}/{productname}-p{product}
So, path
is a variable category structure containing even forward slashes, in this example et--zubehoer-c16/zubehoer-c48/top-cases--taschen-c63
. productname
would be top-case-32l-rosso-passione
and product
would be 64
.
This route logic is existing in a Symfony backend that I'm now replacing with Angular. Therefore, I need to implement the same logic. This is the Symfony route definition:
// Product detail pages
@Route("/{path}/{productname}-p{product}", name="shop-detail", defaults={"path"=""}, requirements={"path"=".*?", "productname"="[\w-]+", "product"="\d+"})
// Category pages
@Route("/{path}", name="shop-listing", defaults={"path"=""}, requirements={"path"=".*?"})
I would like to have two handlers for this route:
/shop/
/shop/et--zubehoer-c16/
/shop/et--zubehoer-c16/zubehoer-c48/top-cases--taschen-c63/
pXXX
where p
indicates that it's product detail page, e.g.
/shop/et--zubehoer-c16/zubehoer-c48/top-cases--taschen-c63/top-case-32l-rosso-passione-p64
The problem here is of course that the first handler has a variable amount of sub-directories. What I could imagin is to differentiate these two with the ending p{product}
. If this exists in the URL, the product detail handler should be called otherwise the category handler.
A first attempt to use the following didn't work:
const routes: Routes = [{
path: 'shop',
children: [{
path: '',
pathMatch: 'full',
loadChildren: () => import('product-listing').then(m => m.ProductListingModule),
}, {
path: '**/p:id',
loadChildren: () => import('product-detail').then(m => m.ProductDetailModule),
}]
}, {
path: '**',
component: NotFoundComponent
}];
Instead of using the path
attribute in your routes, you can use the poorly documented matcher
attribute (check it on the docs). You probably haven't heard of it because it's not that common. But basically you provide a function that takes the paths segments (actually, an array of UrlSegment
=> each UrlSegment
contains a path
attribute referring to an element of the array produced by path.split('/')
). If the matcher function returns null
, it means that you haven't found a match. If it returns the array of path segments, it means it's a match.
So, you can define your matcher as:
// matches /shop/{path}/{productName}-p{product}
export function productMatcher(url: UrlSegment[]) {
// The path must start with 'shop' and have more than 1 segment
if(!url || url.length < 2 || url[0] !== 'shop') {
return null;
}
// The last segment is supposedly your product
const productSegment = url[url.length - 1].path;
// The 'g' option (global search) is mandatory for the
// regex.exec(...) below to work right
const regex = /([a-zA-z0-9-]+)(-p)(\d+)$/g;
// If it doesn't match the regex, it's not a product: it's a category
if (!regex.test(productSegment)) {
return null;
}
// To the regex.exec(...) function work right, you must reset the index
// because it was messed up by regex.test(...) function
regex.lastIndex = 0;
const m: string[] = regex.exec(productSegment);
// If there are matches, m is different from null
if (m) {
const [whole, productName, _, product] = m;
const category = url
.slice(0, url.length - 1)
.map(x => x.path)
.join('/');
// Return the original segments, and add some parameters
// to be grabbed from the paramMap.
return {
consumed: url,
posParams: {
category: new UrlSegment(category, {}),
productName: new UrlSegment(productName, {}),
product: new UrlSegment(product, {})
}
};
}
return null;
}
Then, in your routes config:
const routes: Routes = [
{
matcher: productMatcher,
component: ProductComponent
}
];
And in the component:
constructor(route: ActivatedRoute) {
route.paramMap.subscribe(m => {
this.productName = m.get("productName");
this.product = m.get("product");
this.category = m.get("category");
});
}
In a similar way, you can build a matcher for the categories. Looking at the product matcher, if among its segments we find the term shop
, and it's not a product, it has to be a category (at least by the conditions mentioned in the question text.
// matches /shop/{path}
export function categoryMatcher(url: UrlSegment[]) {
if(!(url && url.length && url[0].path === 'shop')) {
return null;
}
// Just '/shop'
if (url.length === 1) {
return return {
consumed: url,
posParams: {
category: new UrlSegment('', {}),
}
};
}
const lastSegmentPath = url[url.length - 1].path;
// Every category (except shop) finish with a dash followed by a
// letter different from "p" followed by one or more numbers
const categoryRegex = /(-[a-oq-zA-OQ-Z])(\d+)$/g;
if (!categoryRegex.test(lastSegmentPath)) {
return null;
}
const category = url
.map(x => x.path)
.join("/");
return {
consumed: url,
posParams: {
category: new UrlSegment(category, {}),
}
};
}
And you can add the matcher to your router configuration:
const routes: Routes = [
{
matcher: productMatcher,
component: ProductComponent
},
{
matcher: categoryMatcher,
component: CategoriesComponent
}
];
The order here doesn't matter, because both matchers verify if there's a product in the path to making a decision.
Based on the exposed above, you can do anything you want. The Stackblitz demo shows a more interesting scenario, with a lazy-loaded module, as you want. But there's nothing so different from what I discussed above.