In this blog post, we will discuss how to correctly provide object as inputs in Angular components. As an example,
we’ll take a look at angular-three-cannon NgtcPhysics
component.
NgtcPhysics
is a component that provides the Physics World for THREE.js 3D object using Cannon.js.
Here’s a list of most of the inputs that NgtcPhysics
component accepts with their default values:
Individual Input approach
Normally, we would author NgtcPhysics
with each individual Input for each option
The advantage of this approach is it allows the consumers of NgtcPhysics
to only provide the options they want to override. For example, if the consumers only want to override the gravity, they can do so like this:
However, the above approach has a few drawbacks:
- Some inputs are missing type information. This is easy to fix though by providing the type argument to
input()
- Second drawback is that it is hard to observe whenever any input changes, kind of like
ngOnChanges
. Moreover, it is also tricky to use all the inputs as an object as an option object to something. For this particular example, we’re using the inputs to create aCannonWorkerAPI
We can fix this second drawback by creating a computed
with all the inputs
This is verbose and will be tedious when we have more components that require this approach. As great Angular developers as we all are, we will probably go ahead and create a Custom Inject Function (CIF) for this.
Please take a look at ngxtension inputs for the implementation of this CIF
With that in mind, maintaining components with many inputs with default values is somewhat troublesome with all the workarounds. Here comes Object Inputs
Object Input approach
Personally, I’ve voiced my opinion on this before in regards to Props Spreading and many people have told me to use Object Inputs. While I solved the issue with Services but that is also sub-par and we didn’t have Signal Input at the time.
Let’s revisit NgtcPhysics
and turn out inputs into a single options
input, with the defaultOptions
object as well
This immediately solves both drawbacks of the individual input approach. We don’t have to type each individual input and we can use options()
signal directly without any tedious workaround. Additionally, we can also mark options
as a required input with input.required
which is awesome.
However, we have a big issue that is the consumers cannot override what they only need with our input()
as-is
This does not work because the object { gravity: [0, -20, 0] }
that the consumers pass in will override the entire defaultOptions
; so the other default options are lost.
The fix is simple though: transform
is the answer
Now when the consumers pass in { gravity: [0, -20, 0] }
, the gravity
will override the defaultOptions.gravity
but the rest of the default options are preserved.
We can now make a reusable transform function to reuse this logic across multiple components/directives
Let’s refactor NgtcPhysics
Conclusion
Object Inputs are a great way to provide a single input with default values. This approach is especially useful when we have many inputs with default values. It also allows the consumers to override only the options they want to change. We can also reuse the transform
function across multiple components/directives to keep our code DRY.
Have fun and good luck!
Bonus: Directive with Object Inputs
Directives can use the same approach as the Components. However, the consumers can use our directive with the following approach:
That’s right! someDirective
can match SomeDirective
with [someDirective]
as its selector. Imagine someDirective
is also an option input with default values, Angular Compiler will throw an error saying '' is not assignable to OptionsType
The root cause is when we use <div someDirective></div>
, we provide an empty string ''
to someDirective
input and that does not pass the type-checking for Partial<OptionsType>
. We can fix this by modifying mergeOptions
function as following:
Now the consumers can use <div someDirective></div>
without the compiler errors.