I recently shared my thoughts on Twitter about how Angular is missing Props Spreading, resulting in a subpar experience with Component Composition. Let’s take a moment to explore what Component Composition means in this context.
What is Composition?
Composition, particularly Component Composition, is a pattern of composing different UI Elements to build other UI Elements. In Angular, there are various ways to utilize Component Composition: Content Projection, Directives, Template Ref, and Services
Each provides different solutions to different use cases. Here’s an example:
Using Content Projection
Another example of using Directive to compose UI Elements
Using Directive
Numerous use cases and online resources offer detailed explanations of these methods. Therefore, exploring those resources for an in-depth understanding of Component Composition is advisable.
What is Props Spreading?
Props Spreading is a technique in JSX-based technologies like React or SolidJS. Props Spreading is not an enabler of Component Composition, but it makes it much easier and more strongly typed.
A pseudo-example of an Alert
component with different
severity
levels
Alert
is acceptable to use on its own. However, we can provide some abstractions to make it easier to use. For example, we can create SuccessAlert
, ErrorAlert
, and WarningAlert
so the consumers do not have to keep providing severity
prop for Alert
component.
SuccessAlert
is now easier to use and maintains type definitions
for every other prop than severity
Problem
Back to the tweet, the context here is that I have a NgtsEnvironment
component. This component determines which type of components to render based on some inputs. There are NgtsEnvironmentCube
, NgtsEnvironmentMap
, NgtsEnvironmentGround
, and NgtsEnvironmentPortal
.
It is troublesome for consumers to figure out which one they need to remember to use. Hence, NgtsEnvironment
helps abstract this decision away. The problem is NgtsEnvironment
has the same set of Inputs with all four types. Without something like Props Spreading, this results in many duplications in Component code and on the Template.
Disclaimer:
NgtsEnvironment
is a port ofEnvironment
from React Three Fiber ecosystem. It is already a lot of work to port these components from React over to Angular, so I did not spend additional time attempting to re-design these components to fit Angular’s mental model better. Thus, the complaint 😅
What can we do? Are we completely stuck without some form of Props Spreading? Nope, we can do better.
Inputs Service
The solution here is not entirely on-par with Props Spreading, but it alleviates some pain I am running into. And I want to share it with the world.
Disclaimer: We’ll be using Signals. We’ll use
#
private instead of TypeScriptprivate
keyword. We can debate either in real-world situations because#
makes Debugging a little trickier.
As mentioned above, Service is also a means to achieve Component Composition in Angular. Instead of duplicating the Inputs on all components, we can have the NgtsEnvironment
declare the Inputs and a Service to store those Inputs’ values. The other components (NgtsEnvironment***
) can inject the Service to read those Inputs.
Before we start, we will shorten our Inputs to 5-6 and render 3 components: NgtsEnvironmentGround
, NgtsEnvironmentCube
, and NgtsEnvironmentMap
for brevity. Let me remind everyone what NgtsEnvironment
template would look like with 3 Inputs and 3 component types.
Let’s start with our solution now.
Here, we set up a NgtsEnvironmentInputs
Service to store the Inputs’ values. Naturally, we can use any data structure to store the values. In this example, we use Angular Signals.
We provide NgtsEnvironmentInputs
service on NgtsEnvironment
providers, so for every new instance of NgtsEnvironment
, we’ll have a new instance of NgtsEnvironmentInputs
. Instead of duplicating the Inputs on NgtsEnvironmentGround
, NgtsEnvironmentMap
, and NgtsEnvironmentCube
, these components can inject NgtsEnvironmentInputs
and use the computed
values. With this change, NgtsEnvironment
template now looks like the following:
It is a lot cleaner! However, we do have a problem which is the default values for the Inputs. Each component can provide different default values for NgtsEnvironmentInputsState
.
At first glance, we might be thinking of doing this
But this would not work because of how Angular resolves Inputs. Here’s the over-simplified timeline:
<ngts-environment [map]="mapFromConsumer()" />
is used by the consumers and they provide some InputsNgtsEnvironment
is instantiated along withNgtsEnvironmentInputs
NgtsEnvironmentInputs
storesmap
value from@Input() set map()
NgtsEnvironment
renders<ngts-environment-map [map]="environmentInputs.map()" />
NgtsEnvironmentMap
is instantiated, and itsconstructor
runs. Here,NgtsEnvironmentInputs
updatesmap
withdefaultForEnvironmentMap
, which is problematic.
We can work around this with two steps:
- Duplicate
map
Input onNgtsEnvironmentMap
(similarly, we’ll need to duplicatebackground
Input onNgtsEnvironmentCube
)Which Input needs to be duplicated is varied on a case-by-case basis.
- Implement a
patch
method in ourNgtsEnvironmentInputs
service
Next, we can update NgtsEnvironmentMap
Finally, NgtsEnvironment
template looks like
Conclusion
Inputs Service is a pattern that can help with Component Composition when duplicating Inputs exist. It is a lot of code than Props Spreading, but it does get the job done. In addition, Inputs Service can contain encapsulated logic related to the Components at hand instead of just storing the Inputs values. I hope this blog post has been helpful to you. See you in the next one.
(Bonus) Signal Inputs
In the near future, we will have Signals Inputs, which will change our approach a bit. Instead of a Service, we would have an Abstract Directive