For simple use cases, it is a simple process to setup Angular routing: create a few components or modules, then declare an array of routes linking those components with url paths. However, proceeding in that manner leads to a coupling between the url path and the component or module to be loaded. This coupling is not necessarily an issue in most cases, but it will not be viable in a situation where we will want to create different versions of pages and choose which version to display to the user while keeping the same url path regardless of the selected page.
Routing with Versioned Pages
On the www.bytethisstore.com website, we are versioning certain pages so that we can configure which version of the page to display to the user. This gives us a certain degree of flexibility:
- We can asynchronously determine which version of the page to use after the application has been initialized.
- We can display a version-in-progress in a lower environment while displaying the existing page in the production environment.
- We can choose which version of the page to display in each environment via app configuration.
- We will be able to perform A/B testing on pages dynamically without changing any route declarations.
- If we discover an issue in production, we can quickly switch to a different version of the page while we troubleshoot the issue.
- We can implement a functionality where the user can choose which version of a page they want to see and update dynamically without a page reload.
In the steps below, we will outline how to convert an existing component/module route to a versionable route. If you want to create one from scratch, the steps are almost identical. These steps will show using components in the routes, but the same concept applies to lazy loading.
Step 1: Move the Existing Component/Module
Let's say we have a component GreenComponent
which we want to version as ColorComponent
. The existing code looks something like this, with the component, routing, and module declarations:
We want to apply versioning to the root path so we can display different versions of the color component. To get started, we will move the logic of the existing component to a new one:
We have extracted the original logic into its own component under /versions
. This is where we will maintain the versions of the page. The original component is empty for now; we will add some logic in a later step. For now, we can proceed to the next step.
Step 2: Create A New Version of the Component
If you already have the code or concept for creating another version, you can implement that new component under the versions folder. If not, create a dummy component, as this will still facilitate the process (the dummy component can be deleted later). In this example, we'll create another type of color component.
Now that we have two versions of code to work with, we can proceed to the next step.
Step 3: Create a Service with Determines which Version of the Page to Display
To continue, you will need to know how you will be able to determine which version to route to. There are a few different possibilities, including but not limited to:
- Reading from an app configuration object.
- Using a service which handles A/B testing.
- Determining based on some user account preferences.
- Hard coding the value in a service (not recommended other than for debug purposes).
In this example, we'll use a service which reads some configuration from the backend called ConfigVersionReader
. We will omit the implementation details here. We can now proceed to the next step.
Step 4: Implement the Versioning Routing
In this step, we will put the pieces together to implement the routing. The flow will look like this:
- The user will navigate to the Colors page.
- The initial ColorsComponent will determine which page to route to based on the service from the previous step.
- Based on the currently selected version, the component will use the router to route to the selected version.
The original component is now responsible for determing which version to route to, via the service from the previous step, and initiating the route redirect Note: this will add the version name path to the url. When we attempted to use skipLocationChange: true
in our own implementation, we discovered that updating the query params later on re-introduced the additional path name. Therefore, if the selected version is SecondaryColors
, the route will end up being '/colors/secondary-colors'. If you do not need query params, this will not be an issue.
At this point, the versioning code is implemented. There is one final step which needs to be implemented in order to prevent users from directly accessing non-active versions.
Step 5: Guard Against Accessing Non-Selected Versions
We now need to guard against the following scenario:
- The user visits a page and is redirected to the appropriate version.
- The user bookmarks that url, or shares it on social media, with the version path in that url.
- At some later date, the active version is changed.
- Some user attemps to visit the url with the old version path.
With the code as it is now, the old version of the page will still display. If the desired behavior is to redirect to the currently active page, we will need to implement a route guard. When the path to any version is navigated to, the guard will confirm the version is active/selected. If it is not, it will redirect back to the initial component, which will from there, route to the correct version. The service will look something like this:
Now, if the user attempts to navigate to an non-selected version, the system will route them away from there and to the selected version.
Putting it all together, we have
A Note on Lazy Loading
The approach for versioning when lazy loading components is almost exactly identical. The only differences are:
- The versioned components will be declared in their own modules instead of the page module.
- The route declarations will use loadChildren instead of component.
This is the recommended approach. If we do not lazy load, the module will load all components at once even though only one will be used, which can potentially be a costly operation.
Abstracting Away Similar Functionality in Components
In most cases, different versions of the same page will have the same, or at least similar, functionality. If each version handles its own logic entirely, there will be a potentially large degree of overlap and even code duplication. Therefore, we recommend abstracting away the common functionalities. View view-model approach will solve this issue by separating the logic and data models from the components:
- Determine which functionalities are specific to each component and which are common.
- Create a view model class and inject into each version's component.
- Move the common logic into the view model.
- Any place which requires the common logic will now reference the view model directly.
This approach will keep code quality higher for existing code and facilitate the process of creating new page versions. Now, the only thing you will need to do when creating a new page version is handle the logic specific to the UI presentation.