Demystifying a Common “Bug” Encountered by Developers new to Angular
I often work with clients that are transitioning their legacy MVC or WebForms web applications over to using Angular and Web API instead. After the initial ramp-up period with the framework, I’ve often had developers on these teams tell me how much easier it is to debug logic in Angular versus “how it used to be.” Writing logic with Angular and TypeScript provides these developers with a level of code organization on the front-end that they’ve previously only seen in their back-end .NET code. As a result, I think the framework especially appeals to developers who have previously shied away from front-end development.
To replicate the kind of situation developers run into when encountering this issue, I’ll be demonstrating component interaction via input binding. Input binding is one of the simplest ways to share data between components in Angular. If you need a refresher, visit the official documentation on component interaction from Angular.
Consider the diagram below:
I’ve set up a simple application in Angular consisting of two components that have a parent-child relationship. In this example, specifically an obje each component has a counter that is displayed in its respective template. I’d like to connect these two counters with input binding so that they always display the same value. If I perform an addition or subtraction operation in the parent, it should be reflected in both components. If I perform a reset operation in the child and set the child counter to 0, the parent counter should also be reset. This behavior is referred to as two-way binding and is frequently employed between components. To begin, look at how I implemented the code for the ChildComponent class:
On line 4, we see that the selector for this component is ‘app-child’—we’ll use this when creating an instance of this component in the ParentComponent’s template. Line 9 is where we initialize and expose the public class variable childCounter for input binding via the @Input() attribute. Finally, line 15 defines a simple function to reset the childCounter to zero. You can see this used in the ChildComponent’s template below:
Next look at the code for the ParentComponent class below:
In the ParentComponent class, we see that parentCounter is just a public class variable that we perform various actions on. The functions add() and subtract() are public and attached to buttons on the ParentComponent’s template which is shown below. Note that on line 13 the ChildComponent is referenced via its selector and its exposed childCounter variable is bound to the parentCounter.
Let’s see what this looks like in action:
Clicking on the Add and Subtract buttons works as expected, when the parentCounter changes the childCounter does as well. So far so good but, what happens when “Reset Counter” is clicked?
What this means for the example above is that parentCounter and childCounter are maintaining their values in two separate locations in memory. The code is simply copying parentCounter’s value to childCounter’s address in memory via Angular’s input binding. Note that updates made specifically to the childCounter are not affecting the parentCounter because input binding only works from parent to child component.
Instead of using input binding with a primitive I’ll use a non-primitive this time; specifically an object. By doing this, I can take advantage of “pass by reference,” so the parent and child components are manipulating data in the same place in memory. Line 10 is where I define this object as an instance of InputModelA. InputModelA has a single parameter representing the counter; its class definition is shown below.
The last bit of refactoring involved changing my references to childCounter to reflect the new childInputModel. These changes are reflected on line 17 of the ChildComponent class and line 5 of the ChildComponent template below.
Lastly, I refactored the ParentComponent in a similar way so that it also used InputModelA. Look at the ParentComponent class and template below.
Take note that on line 12 of the ParentComponent template I am instead using input binding on the inputModel and NOT its counter property.
With all these changes in place, let’s perform the same actions as last time. As shown below, adding and subtracting work as they did before.
Resetting the counter from the child component now also works; thus, we’ve achieved the two-way binding we were looking to implement!
Now you may be thinking, “Ok so if I want two-way binding on a primitive data type, I’ll wrap it in a model. An array is a non-primitive; therefore, I can get by without wrapping it in a model.” This logic is sound; however, there is a situation that will break your array’s two-way binding. Consider this second example which works just like the first except it passes in an array instead of a primitive:
Running this example, we see that both arrays display the same items as they are removed from the ParentComponent. We can even remove all the items in the array then add more back, and the two-way binding between the components remains.
Going back to our array example; one solution is making sure never to assign childArray to a new array in ChildComponent. I find that hard to enforce, especially with multiple developers in the source code—this solution seems unintuitive. An alternative is anchoring down your reference to your non-primitive by wrapping it in an object (a non-primitive) as we did with the counterexample. See InputModelB defined below:
By wrapping your non-primitive in an object, you’ll be passing a reference that contains another reference. By passing an instance of InputModelB from ParentComponent to ChildComponent via input binding, you’re safe to reassign the array parameter. That’s because both components will still share a reference to an object that will point to that new array’s address in memory.