There is a human tendency to start small, then overshooting by doing too much of one thing and then try to find the golden middle way. This is happening in basically everywhere in life, eg. you start to work out, you work out too much and get injured and then you find just the right amount that you can stick to. Knowing this pattern in life can help you predict the future and save yourself from the painful overshoot by having awareness of the two extremes and try to position yourself in a place in-between.
In Angular development, it is no different. We keep jumping over a new shiny object (RxJS, Redux, PWA, functional programming, you name it), over-abuse it and then sometimes toss it away because it didn’t work as intended anyway. But the tool actually had a purpose in itself but stopped working because it got used like an all-purpose tool everywhere.
A common scenario in Angular apps which includes a questionnaire is to start with hardcoding. Then, as hardcoding becomes harder to maintain, the team tries to render the questionnaire dynamically with data from an API. This is, in theory, a good idea, which is why this solution is commonly implemented as it provides “easy” changes and centralization of the questionnaire. It also supports that the questionnaire can be used by multiple clients using reusable components. Then reality hits you and you are now having a hard time doing simple changes because everything is tied up in the questionnaire rendering engine. You start longing for the good old times when the questionnaire was hardcoded and simple changes were easy.
This post is going to cover how you can find the perfect balance between these two paradigms that will give you the maintenance benefits of a centralized questionnaire while having the flexibility of being able to design each question as you like.
Let’s go back to where it all begins.
Starting out by hardcoding
In the example with the questionnaire application, you might have a system where you need one app for answering the questionnaire and then one app for a back-office to see the answered questionnaire. To begin with, the questionnaire is hardcoded in both apps and all is good. As the questionnaire gets modified it becomes tiring for the developers to update both the client and the back-office app. Then one of the smart developers suggests that you centralize the questionnaire on a separate service and then the two apps are being generated dynamically by “simply” making changes to the questionnaire service. All sounds good and the teams build the questionnaire service and start rendering the questionnaire dynamically using the questionnaire data.
The monster grows
The business and UX’ers keep asking for changes and the developers keep expanding the question rendering engine in the Angular apps to handle new question types, validation types, conditional rules and such, making it more and more complicated to maintain. The poor developers making the question rendering engine more and more complicated until the UX team asks you to do some simple change to the UI but you can’t because your apps are too coupled to the rendering engine which doesn’t allow for individual styling for specific questions. The initial idea with having this questionnaire service and the questionnaire rendering engine on the apps was to spend less time on changes, but now you are actually spending a lot more time than if you were hardcoding.
Fighting the monster by doing a hybrid approach
Now it becomes clear that you must be able to handle individual changes for the app by making it more flexible. In my post about the painful trap with reusable Angular components I explain how you, instead of making the component interface more and more complicated as the complexity of the reusable component increases, should consider transclusion, by passing a templateRef to the component which will determine how some part of the reusable component will be rendered in a flexible manner.
Enable hardcoding in your dynamic questionnaire app
For demonstration purpose, I am going to expand upon my questionnaire render demo which is where I show you how to create a dynamically generated questionnaire like we have been talking about until now. What we want to do is add support for hardcoding specific questions, so we can both enjoy the centralization of the questions and dynamic rendering while having flexibility when we need it. If you need to create a new question, which is not supported by the question rendering engine, you can simply code it as a component for that specific question. You can find the complete code on my Github.
The app structure will end up looking like this:
Here we have created a module for the hardcoded questions, which are being registered in a service (HardcodedQuestionsService) as a map of templateRefs. This is then used in the question component, which if it is a hardcoded question, will lookup in the hardcoded questions map and instantiate the corresponding templateRef. The hardcoded question is added to the questionnaire JSON data, but the app will only use the question id and type, as the question will not be dynamically rendered like the other ones.
This is easy to do and it will save us for a lot of headage in the long run. Let’s dive into the specifics.
Creating and registering the hardcoded questions
First, we create some question we want to hardcode to look a specific way:
When this question gets added to the hardcoded questions component, which will set up the templateRefs for all the hardcoded questions. Note how let-question="question"
is enabling us to pass the question using ngTemplateOutletContext
.
In HardcodedQuestionsComponent
the hardcoded questions templateRefs get selected using ViewChild
and they are being added to the hardcodedQuestionsMap
.
This makes these hardcoded questions globally available in the app through the HardcodedQuestionsMapService
.
Let’s look at how to render these hardcoded questions.
Support hardcode questions in the question renderer
The hardcoded question is being rendering by checking if the question answerType is “hardcoded” and then it will get the hardcoded questions templateRef from the hardcodedQuestionsMap
.
Add the hardcoded question to the Questions file
The hardcoded questions simply have a type indicating that this is hardcoded, the question id and text:
Now the questionnaire supports both renderings of standard question types as well as the flexibility to render hardcoded question.
Conclusion
In this post, we went through a common cycle in Angular development that goes as follows: start by hardcoding, then over-engineering with dynamic rendering but sacrificing flexibility and then lastly a correction to this by supporting the flexibility of hardcoding specific parts of the app while keeping the dynamic rendering engine. Having this gives us the best of both worlds and we can generalize the hardcoded parts and integrate them in the dynamic rendering engine if they become a common reused pattern.
Do you want to become an Angular architect? Check out Angular Architect Accelerator.
2 thoughts on “The Hardcoded, Dynamic and Hybrid Approach in Angular Apps”
Nice to look into this common problem. But forgive me, why couldn’t you just add any componets to the ng-switch statement? I’m not understanding the benefits of your approach over ng-switch? – you still need to register the componet names beforehand, which are stilll eager loaded entry componets in the module and still only rendered on the screen if applicable?
Ng-switch is also an option but I prefer to keep the templates as slim and dumb as possible. Also, it is worth to look into dynamic creation of components, instead of the templateRefs for inserting the hardcoded parts:
https://angular.io/guide/dynamic-component-loader