Adding Structure to If Statements with help from Angular

skye simpson
Trade Me Blog
Published in
8 min readOct 30, 2022

I regularly contribute to the Frontend at Trade Me. It’s large, single page application that is built using Angular. I recently encountered an interesting architectural problem that I’d like to share.

What problem are we looking to solve?

My team and I were building a feature called Recent Searches, which are clickable cards displayed on the homepage that show key information about your most recent search. They will re-run that search with all the same parameters and filters when selected to take members back precisely to their previous search. The idea is to make it easy for our members to easily jump in and re-search what they are looking for 😁.

What’s a vertical, and how does it impact what’s shown?

Our Recent Search cards have different designs for each vertical, highlighting the search parameters and filters that are most relevant for that vertical. A vertical at Trade Me is also sometimes called a business unit, and it’s an umbrella category referring specifically to Marketplace, Property, Motors and Jobs. The vertical will impact what is shown, as every vertical has search filters and parameters that only apply to that vertical. For example you would ONLY search for an Odometer under 100,000km if you were searching in motors. Likewise pay type hourly or annually would only apply when you are searching for a job.

recent search cards displayed on the trade me homepage

Here’s a couple of examples of cards for various verticals.

recent search cards

How was the problem solved previously, and what were the issues with it?

Previously, this problem was solved by having one Recent Searches component that is responsible for ALL the verticals in one file. Things had started to get a little messy and we were having to do a lot of logic and many if statements to filter out the search refinements we needed to show based on which vertical we were currently in.

<strong *ngIf="recentSearch.areaOfBusiness === areaOfBusinessEnum.property && recentSearch.keywords ">..
</strong>
<ng-container *ngIf="recentSearch.breadcrumbs && recentSearch.areaOfBusiness !== areaOfBusinessEnum.jobs">...
</ng-container>

This was feeling like a bad code smell for a few reasons. When we wanted to add another card here, for example “Stores Searches”, it was quite hard to do. We had to make sure we didn’t break any existing logic. The manual testing for even small changes was substantial. There were a lot of side effects and you had to be worried about breaking things as everything was all tightly coupled. Furthermore this approach violated the “Single Responsibility Principle” of SOLID.

The solution to the problem is shown below with display logic for each vertical all kept together in one file.

diagram of how recent searches code was structured

How did we re-build it?

We decided that this problem could be solved, maintained and scaled in a more reliable way using a polymorphic design that takes advantage of composition. In Martin Fowler’s book Refactoring he discusses this at length as the Replace Conditional with Polymorphism refactoring pattern. The complex conditional logic of the recent-searches.component could be extracted into separate components and composed into a single component using dependency injection.

We made a component called the recent-searches-card-switcher that is only responsible for projecting each of the search cards and can switch between different card types. It uses a component factory resolver to make a generic component which is filled in with the vertical specific component based on the result of the injection token and an isMatch function.

diagram of recent searches card switcher component
export class RecentSearchesCardSwitcherComponent implements OnChanges, OnDestroy {
@Input() public recentSearch: IRecentSearch;

@ViewChild('recentSearchCardHost', { read: ViewContainerRef, static: true }) public recentSearchCardHost: ViewContainerRef;

private _componentRef: ComponentRef<IRecentSearchesCardComponent>;

constructor (
@Optional() @Inject(RecentSearchesToken) private _recentSearchCards: Array<IRecentSearchCard>,
private _componentFactoryResolver: ComponentFactoryResolver
) { }

.....

const cardItem = (this._recentSearchCards || [])
.find(item => item.isMatch(vertical, seller));
....
}

How did we achieve this?

The main concepts from the Angular Framework that help us achieve this approach are dependency injection — specifically injection tokens.

Using dependency injection in our app

There are some good resources for learning about dependency injection in an angular application such as the Angular docs.

Angular has a hierarchical tree of injectors, with the root injector at its root. If we register a dependency with providedIn: root, then it will be registered at the root injector. This is great for most of our dependencies, because it means that if two components in different parts of the application ask for that dependency, they will get the same instance.

However in our use case we have our components that have dependencies on an abstraction ( card-switcher). That abstraction has different implementations in different verticals. How we manage this is by using an injection token to be able to depend on that abstraction. The injection token is a simple string value.

diagram of recent searches token
export const RecentSearchesToken = new InjectionToken<IRecentSearchesCardComponent>('recentSearchesCardItemToken');

We can’t register our concrete implementations in the root injector because they need to be registered with the same injection token. There will either be an error if you try to register things twice under the same token (without specifying multi: true) or the last one wins (which could introduce unwanted side effects).

In our case we want to specify multi: true in the modules of the specific card implementations that have been split out. Here the provider takes the injectable token and the useValue key is what allows you associate a fixed value with a DI token - in our case the value of the type of component that is passed in as below.

diagram of provider
export function getRecentSearchesCardProvider (component: Type<IRecentSearchesCardComponent>, isMatch: isMatchFn): ValueProvider {
return {
provide: RecentSearchesToken,
useValue: { component, isMatch },
multi: true
};
}

This gets passed in inside the provider of the Module of each specific implementation (in our case we have five different components) — an example of one being called in the module is below.

diagram of marketplace recent search module
export class MarketplaceRecentSearchCardModule {
public static toMatch (matcher: isMatchFn): ModuleWithProviders<MarketplaceRecentSearchCardModule> {
return {
ngModule: MarketplaceRecentSearchCardModule,
providers: [
getRecentSearchesCardProvider(MarketplaceRecentSearchCardComponent, matcher)
]
};
}
}

The modules all get registered together in the all-recent-search-cards.module.ts which means all the dependencies can be pulled in and grouped together when in use.

Diagram of all recent search cards module
@NgModule({
imports: [
PropertyRecentSearchCardModule.toMatch(isPropertyRecentSearchCardMatch),
MarketplaceRecentSearchCardModule.toMatch(isMarketplaceRecentSearchCardMatch),
JobsRecentSearchCardModule.toMatch(isJobsRecentSearchCardMatch),
MotorsRecentSearchCardModule.toMatch(isMotorsRecentSearchCardMatch),
SellerRecentSearchCardModule.toMatch(isSellerRecentSearchCardMatch)
]
})
export class AllRecentSearchCardsModule { }

Along with passing in their specific matcher function

export type isMatchFn = (areaOfBusiness?: AreaOfBusinessEnum, seller?: IMember) => boolean;
export function isMarketplaceRecentSearchCardMatch (areaOfBusiness?: AreaOfBusinessEnum, seller?: IMember): boolean {
return areaOfBusiness === AreaOfBusinessEnum.marketplace;
}

This way we make sure that components like the card-switcher can live in main (a central file location) but depend on a vertical specific implementation. The card switcher pulls all the modules in via an injection token. For each new card that gets rendered it uses the isMatch on every recent search card to know which card to display. The card switcher has all the available cards injected into it - it doesn't decide how to display the layout or details - it simply asks the interface which card to display.

This diagram outlines how all the pieces fit together, with the runtime result of the injection token and isMatch function deciding which verticals card to render, and the display logic in the component html only being concerned with visual layout and not a lot of business logic mixed together anymore 😎.

Diagram of new recent searches architecture

The example in stackblitz also shows the working pieces fitting together.

How is it better?

This new way of doing things has effectively moved the decision making based on verticals, out of the component view level and up into a generic component which gets passed the information it needs from the module providers. This allowed each vertical to now have it’s own component which only relied the logic for its own set of search rules. For example property only needed to worry about property concerns, and marketplace is only responsible for marketplace things 😎. By the time the component is rendered the decision of “which search am I?” has already been made at the switcher level, and it can just render all the information it is fed via an input.

This makes for a very straight forward UI component, which is more precise to work in. Unit tests and manual test are also more modular and have no side effects. Furthermore an arbitrary number of cards can be added and it won’t affect the overall complexity of the project.

This new solution design also illustrates three SOLID principles. Firstly the “Single Responsibility Principle” — each vertical card cares only about rendering that vertical’s own component.

Secondly the “Open-Closed Principle” whereby it’s available to extend in adding extra cards, without modifying the interface itself.

Lastly the “Dependency Inversion Principle” (arguably the most important SOLID principle 😎). Previously the recent-searches.component implemented all of the logic for every vertical card, and therefore depends on the details of the implementations. In the new design, the recent-searches-card-switcher-component defines an interface, IRecentSearchCardInterface, and each card module implements that interface. The dependency has been inverted 🔥🔥.

Pros

  • Complex conditional logic can be hard to reason about — this removes lots of the previous if statements from a formerly large file👏.
  • Any changes to an individual card affect only that card — increasing the ease and speed of development 👏.
  • Unit tests and manual test are more modular and have no side effects👏.
  • An arbitrary number of cards can be added and it won’t affect the overall complexity of the project- making it scalable 👏.
  • SOLID principles are honored — specifically “Single Responsibility”, “Open-Closed” and “Dependency Inversion” 👏.

Cons

  • Avoiding circular dependencies can be tricky with this approach.

Conclusion

When working in a large scale application, where code can easily become overly complex it is really appreciated where there are strategies and patterns we can use to make things less reliant on each other and more manageable 😎

What’s next?

In future we could swap out the component factory resolver for the dynamic component loader as the resolver is now deprecated. As well as iterating on this feature based on the feedback from our ab testing. Watch this space 👀

--

--