Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Matthew Eash
Ben Nadel at NCDevCon 2016 (Raleigh, NC) with: Matthew Eash@mujimu )

Manually Triggering ngOnChanges() Inside An NgModel Value Accessor In Angular 2 Beta 11

By Ben Nadel on

Earlier this week, I stumbled over the fact that Angular 2 won't trigger the ngOnChanges() life-cycle method if the component inputs are changed programmatically; this change detection integration seems to be tightly coupled to the property binding template syntax. This realization got me thinking about the difference between public properties and "Input properties;" and, the kind of contract that is assumed when exposing public properties as component Input properties. This "Input property" contract complicates the use of ngModel because ngModel acts as a proxy to those Input properties. This means that ngModel changes the proxied Input properties programmatically (via the value accessor bridge); which, in turn, means that the ngOnChangs() life-cycle method won't be triggered by Angular. So, in order to uphold the Input property contract, we have to figure out how to manually trigger the ngOnChanges() life-cycle event from within our ngModel value accessors.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

CAUTION: Due to the extreme complexity of this situation, I have to assume that something in my thinking is just plain wrong. Perhaps Input properties don't really incur an unspoken contract? Perhaps you should never depend on the ngOnChanges() life-cycle event being invoked? Of course, neither of those thoughts feel "correct" either. All to say, take the following with a grain of salt. I think I might be off in no-mans-land.

When the ngOnChanges() life-cycle event is triggered, it is passed a collection of SimpleChange objects. Each of these objects has an instance method, isFirstChange(), which allows us to differentiate between input initialization and input update. If we are going to trigger the ngOnChanges() life-cycle method manually, then we have to make sure that our SimpleChange objects follow the same behavior.

The problem with this requirement is that, under the hood, the SimpleChange class is using an internal token to denote an uninitialized value:

ChangeDetectionUtil.uninitialized

Angular 2 Beta 11 has chosen not to export this value in any of its barrels (at least not that I could find); which means that when we set up our manually-created SimpleChange objects, we have no way to pass this internal token as the initial "previousValue" property of the SimpleChange constructor. To get around this, we have to track the first write (of the ngModel directive) and then explicitly override the isFirstChange() instance method during the first round of changes.

ASIDE: Like I cautioned above, this is so complex, one has to assume that it is the wrong approach.

To explore this problem, I've created a simple Toggle component that takes a "value" input and emits a "valueChange" output sequence. Then, I've proxied the value Input using the ngModel and provided an NG_VALUE_ACCESSOR bridge that manually triggers the ngOnChanges() when the following actions occur:

  • The value is changed externally to the Toggle component.
  • The valueChange is emitted by the Toggle component and needs to be piped back into the Toggle component as part of the two-way data-binding workflow.

The most interesting code is as the bottom:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Manually Triggering ngOnChanges() Inside An NgModel Value Accessor In Angular 2 Beta 11
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></lin>
  • </head>
  • <body>
  •  
  • <h1>
  • Manually Triggering ngOnChanges() Inside An NgModel Value Accessor In Angular 2 Beta 11
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/11/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • requirejs(
  • [ /* Using require() for better readability. */ ],
  • function run() {
  •  
  • ng.platform.browser.bootstrap( require( "App" ) );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root application component.
  • define(
  • "App",
  • function registerApp() {
  •  
  • // Configure the App component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  • directives: [ require( "TOGGLE_DIRECTIVES" ) ],
  • template:
  • `
  • <toggle [(ngModel)]="toggleIsOn"></toggle>
  •  
  • <p>
  • Toggle value: {{ ( toggleIsOn ? "Yes" : "No" ) }}
  • </p>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // I determine whether or not the toggle is currently on.
  • // --
  • // NOTE: We are using ngModel to bypass the one-way data flow and
  • // allow the Toggle component to update this value (so to speak).
  • vm.toggleIsOn = true;
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide all of the directives need to work the Toggle, including the
  • // ngModel variations.
  • define(
  • "TOGGLE_DIRECTIVES",
  • function registerToggleDirectives() {
  •  
  • return([
  • require( "Toggle" ),
  • require( "ToggleForNgModel" )
  • ]);
  •  
  • }
  • );
  •  
  •  
  • // I provide a Yes / No toggle component.
  • // --
  • // NOTE: Notice that the core Toggle component DOESN'T KNOW ANYTHING ABOUT
  • // NGMODEL or the concept of "value accessors." That's a good thing - it goes
  • // beyond the scope of responsibility for this component.
  • define(
  • "Toggle",
  • function registerToggle() {
  •  
  • // Configure the Toggle component definition.
  • ng.core
  • .Component({
  • selector: "toggle",
  • inputs: [ "value" ],
  • outputs: [ "valueChange" ],
  • host: {
  • "(click)": "handleClick()"
  • },
  • template:
  • `
  • {{ ( value ? "Yes" : "No" ) }}
  • &mdash;
  • toggled {{ changeCount }} times.
  • `
  • })
  • .Class({
  • constructor: ToggleController,
  •  
  • // Define the life-cycle methods on the prototype so that they
  • // are picked up at run-time.
  • ngOnChanges: function noop() {}
  • })
  • ;
  •  
  • return( ToggleController );
  •  
  •  
  • // I control the Toggle component.
  • function ToggleController() {
  •  
  • var vm = this;
  •  
  • // I am the event stream for the valueChange output.
  • vm.valueChange = new ng.core.EventEmitter();
  •  
  • // I keep track of how many times the toggle value has been changed.
  • vm.changeCount = 0;
  •  
  • // Expose the public methods.
  • vm.handleClick = handleClick;
  • vm.ngOnChanges = ngOnChanges;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the internal click event on the component.
  • function handleClick() {
  •  
  • // When the user clicks on the toggle, we can't change the value
  • // directly - that would mess up the one-way data flow. Instead,
  • // we have to emit the value change event and let the calling
  • // context decide if it wants to respond by updating the inputs.
  • vm.valueChange.next( ! vm.value );
  •  
  • }
  •  
  •  
  • // I get called whenever the bound inputs have changed.
  • function ngOnChanges( changes ) {
  •  
  • // We're using the ngOnChanges() event to track how many times
  • // the toggle component has been toggled.
  • vm.changeCount++;
  •  
  • console.log(
  • "Changes [first: %s]: %s.",
  • changes.value.isFirstChange(),
  • changes.value.currentValue
  • );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // I provide an ngModel-enabled bridge for the Toggle component.
  • define(
  • "ToggleForNgModel",
  • function registerToggleForNgModel() {
  •  
  • // Configure the ToggleForNgModel directive definition.
  • ng.core
  • .Directive({
  • // Notice that we are only matching on instances of the Toggle
  • // component that also include the ngModel directive.
  • selector: "toggle[ngModel]",
  • host: {
  • "(valueChange)": "handleValueChange( $event )"
  • },
  •  
  • // When ngModel is being used, we need to create a bridge between
  • // the ngModel directive and the target component. That bridge
  • // has to implement the "value accessor" interface. In this case,
  • // we're telling Angular to use THIS DIRECTIVE INSTANCE as that
  • // value accessor provider. This means that the following
  • // controller needs to provide the value accessor methods:
  • // --
  • // * registerOnChange
  • // * registerOnTouched
  • // * writeValue
  • // --
  • // NOTE: You don't need the forwardRef() here because we are
  • // using ES5 instead of TypeScript. ES5 for the win!
  • providers: [
  • ng.core.provide(
  • ng.common.NG_VALUE_ACCESSOR,
  • {
  • useExisting: ToggleForNgModelController,
  • multi: true
  • }
  • )
  • ]
  • })
  • .Class({
  • constructor: ToggleForNgModelController
  • })
  • ;
  •  
  • ToggleForNgModelController.parameters = [
  • new ng.core.Inject( require( "Toggle" ) )
  • ];
  •  
  • return( ToggleForNgModelController );
  •  
  •  
  • // I control the ToggleForNgModel directive.
  • // --
  • // NOTE: Since this controller is performing double-duty as both the
  • // directive controller AND the valueAccessor (for ngModel), it is also
  • // implementing the value accessor interface.
  • function ToggleForNgModelController( toggle ) {
  •  
  • var vm = this;
  •  
  • // As part of the value accessor "bridge" that this directive is
  • // providing, we need to be able to manually trigger the ngOnChanges
  • // life-cycle event on the target component. To do that properly, we
  • // need to keep track of when the first value is written so that we
  • // can announce it as the first SimpleChange instance.
  • var isFirstChange = true;
  •  
  • // Eventually, ngModel will register its own change hander. Until
  • // then, let's start with a no-op to keep the consumption uniform
  • // in the following code.
  • var onChange = function noop() {};
  •  
  • // Expose the public methods.
  • vm.handleValueChange = handleValueChange;
  • vm.registerOnChange = registerOnChange; // Value accessor interface.
  • vm.registerOnTouched = registerOnTouched; // Value accessor interface.
  • vm.writeValue = writeValue; // Value accessor interface.
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the valueChange event coming out of the Toggle component.
  • // Since ngModel doesn't know about this event, we have to bridge the
  • // gap between the Toggle component and the ngModel directive.
  • function handleValueChange( newValue ) {
  •  
  • // Keep track of the values to be used in the ngOnChanges() event.
  • var previousValue = toggle.value;
  • var nextValue = newValue;
  •  
  • // When we invoke the onChange() value accessor method, ngModel
  • // already assumes that the DOM (Document Object Model) is in the
  • // correct state. As such, we have ensure that the Toggle
  • // component reflects the change that it just emitted.
  • // --
  • // NOTE: At this point, we are disregarding the one-way data flow
  • // paradigm. But, that's the WHOLE POINT OF NG-MODEL.
  • toggle.value = newValue;
  •  
  • // Angular won't trigger the ngOnChanges() life-cycle method if
  • // the mutated value is not bound through a template property. As
  • // such, the value accessor bridge has to manually trigger the
  • // life-cycle event handler.
  • triggerNgOnChanges( previousValue, nextValue );
  •  
  • // Tell ngModel so that it can synchronize its own internal model.
  • onChange( newValue );
  •  
  • }
  •  
  •  
  • // I register the onChange handler provided by ngModel.
  • function registerOnChange( newOnChange ) {
  •  
  • onChange = newOnChange;
  •  
  • }
  •  
  •  
  • // I register the onTouched handler provided by ngModel.
  • function registerOnTouched() {
  •  
  • // console.log( "registerOnTouched" );
  •  
  • }
  •  
  •  
  • // I implement the value input invoked by ngModel. When ngModel
  • // wants to update the value of the target component, it doesn't
  • // know what property to use (or how to transform that value to
  • // something meaningful for the target component). As such, we have
  • // to bridge the gap between ngModel and the input property of the
  • // Toggle component.
  • function writeValue( newValue ) {
  •  
  • // Keep track of the values to be used in the ngOnChanges() event.
  • var previousValue = toggle.value;
  • var nextValue = !! newValue; // Cast to boolean.
  •  
  • // Write the ngModel value to the toggle component.
  • // --
  • // CAUTION: Because we know that the Toggle component is not
  • // using any host bindings that depend on this value, we can
  • // safely avoid running into change errors:
  • // --
  • // ExpressionChangedAfterItHasBeenCheckedException
  • // --
  • // Normally, we probably shouldn't make these kinds of assumption.
  • // But, I am trying to keep the [already complex] demo simple.
  • // --
  • // Read more: http://www.bennadel.com/blog/3056-host-bindings-are-breaking-the-ngmodel-bridge-in-angular-2-beta-11.htm
  • toggle.value = nextValue;
  •  
  • // Angular won't trigger the ngOnChanges() life-cycle method if
  • // the mutated value is not bound through a template property. As
  • // such, the value accessor bridge has to manually trigger the
  • // life-cycle event handler.
  • triggerNgOnChanges( previousValue, nextValue );
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I trigger the ngOnChanges() life-cycle event on the toggle
  • // component using the given values.
  • function triggerNgOnChanges( previousValue, nextValue ) {
  •  
  • // If the toggle component doesn't provide a hook for the life-
  • // cycle event, there's nothing we need to do.
  • if ( ! toggle.ngOnChanges ) {
  •  
  • return;
  •  
  • }
  •  
  • var changes = {
  • value: new ng.core.SimpleChange( previousValue, nextValue )
  • };
  •  
  • // Unfortunately, the Angular API doesn't seem to expose the
  • // necessary utility library that is used to denote the "first"
  • // simple change. As such, we have to hack this by overwriting
  • // the isFirstChange() instance method when we know that this
  • // is the first change we are sending to the toggle.
  • if ( isFirstChange ) {
  •  
  • isFirstChange = false;
  •  
  • changes.value.isFirstChange = function() {
  •  
  • return( true );
  •  
  • };
  •  
  • }
  •  
  • toggle.ngOnChanges( changes );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

There's a lot of code here, I know. I tried to walk through it in a meaningful way in the video. But, the result is that when we proxy the Toggle's value Input through the ngModel directive, our Toggle component can still depend on the ngOnChanges() life-cycle event. And, when we run this page and toggle the widget a few times, we get the following output:


 
 
 

 
 Triggering the ngOnChanges() life-cycle event method manually from within an ngModel value accessor bridge in Angular 2 Beta 11. 
 
 
 

I really hope that someone from the Angular team comes along and shows me how all of this crazy complexity can be reduced to one line of code that I missed somewhere. This just doesn't feel right. And it certainly doesn't feel elegant.




Reader Comments

Hi Ben, I stumbled across the very same problem yesterday.
I am reusing a component to edit employees and although the value of ngModel should change, the component does not notice.
My workaround is to have an additional input with the same value, and then get ngOnChanges triggered when a change occurs.

Looks like a bug in Angular2 to me.

Reply to this Comment

@Marius,

That's a pretty clever solution, using another input to get the ngOnChanges to trigger!

It's so hard to say if its a bug. I definitely there's room for the ngModel value accessor to be implemented / integrated better. My guess is that, internally, they only ever use the value accessor to update native form controls, which aren't based on inputs or Angular views. As such, they don't suffer from any of the same kind of constraints that we are trying to bridge.

I am planning on opening a ticket around the "complexity" and not necessarily around any bug. We'll see what comes of it.

Reply to this Comment

Hey @Marius,

How did u get it to work? Can you show me a sample of your code?

I have been trying something similar.
I want to get onChanges() to work when an external change is made in a @Input attribute.

Reply to this Comment

Hey @Ben,

You're very right about how wrong you are. :) You don't need pretty much all that scary stuff you wrote.

Basically all you need is to two things.
1) Implement value accessor and inject external event into it which will be source of changes. Then inject the same event into component and emit those changes inside it on button clicks or whatever events you like.
2) Also you need to subscribe to external changes. You have at least two options here: mark component's property as @Input() or inject NgControl in your component and subscribe to its control.valueChanges event, which is roughly the same.

This will enable you to work with any directives - ngControl, ngFormControl or ngModel - in any combinations. It will take care of all pristine/valid/etc. properties of control in a standard way.

If you have any questions I'll try to answer.

Reply to this Comment

@Alex,

I apologize, I am not exactly sure what you are saying. But, also keep in mind that this approach is an attempt to do this with *any* component, even one that were never intended to be used in with ngModel. For example, imagine some Widget you purchased that you had as a .min.js file and couldn't get the original source. In that case, you can't do anything to the "target component" in terms of using Setters or injecting anything.

Unless you're talking about implementing setters and doing the injection in the ngModel bridge?

Given that, can you maybe explain your answer a bit more?

Reply to this Comment

@Ben,

Well, one thing angular taught me in general that if you're writing a lots of code for something pretty small then you're going in terribly wrong direction regardless of considering anything else. :)

Also looks like I did not make myself clear enough, sorry for that.

May be I got it wrong, but I assumed that you were trying to make some kind of custom component that is compliant with angular's NgForm controls. Regardless of whether you're trying to wire up someone's third party component or write your own, in general you have to provide your own value accessor for that thing, and most likely you'll need to implement your own angular component or directive to initially wire that thing up to your application DOM somehow. My point was that it should really be much simpler than this example and I can show you exact way, just give me some time to create plunker example.

For example, I have successfully integrated select2 4.0 into one of my angular2 apps using pretty much the same approach. But even those couple of directives are not as complex and cumbersome as an example of one button increasing one counter. :) So let me make that plunker example and then we'll continue to discuss based on it, ok?

Reply to this Comment

@Alex,

That would be awesome!

And, just so we're clear - I do *agree* that this stuff is way too complicated. But, I just haven't figured out an easier way to do it yet that upholds what I think are the "public APIs" of the component. This manually triggering the ngOnChanges event is particularly gross because the SimpleChange class doesn't even expose an obvious way to indicate the "first change" as it does so by comparing the value to a private class within the Angular 2 packaging. So, just to get it to work "as expected" from a change-standpoint, it's like hack on top of hack.

Looking forward to what you come up with.

Reply to this Comment

@Ben,

While I'm fighting plunker :) I can give you a couple of hints where to look. First - you don't need OnChanges. Second and most important - regardless of how weird it sounds - you *should not* expose your editing model from control directly together with updating event. Yes, I meant exactly *should not*. If you want to bind its value directly to another component, then this is a job for ngModel, and your component should just support it. It's not component's concern exactly how to bind itself to something outside, and it should not explicitly expose some particular way for it.

For example, let's say you have some control for date editing. It should be bound something like this:

<date-editor [(ngModel)]="date"></date-editor>

Or this:

<date-editor ngControl="someDate" [(ngModel)]="date"></date-editor>

You need value accessor exactly for this. Such a binding will give you full support for standard angular validations and control's state such as pristine/dirty, touched/untouched and valid/invalid. This will fully support working both with and without form. This also enables several components to share one model, i.e. you can just bind two components to one model with ngModel and it will just work. All those features you're missing if you try to implement outside model binding using custom inputs and outputs.

Reply to this Comment

Here is the plunker: https://plnkr.co/edit/KnuEkeisEMW5De74yi08?p=preview

It is in typescript since we're using it widely at work, but it should be pretty straightforward how to translate it into plain javascript. Also it is very simple which should also help to understand it. I put lot of comments our there, but of course you're welcome to ask if something is still not clear.

Hope this will help. :)

Reply to this Comment

@Alex,

This is interesting stuff, though I am not entirely sure I agree with the approach. If you look back at one of my previous comments, my goal here was to implement ngModel *without* forcing the target component to know about it (meaning, ngModel bridged the gap between the target component and the desire to use ngModel functionality). But, in your example, you are basically are forcing the target component (Toggler) to know about both ngControl and the TOGGLE_EVENT.

It seems like you are basically throwing out a number of the things that Angular2 gives you in terms of Inputs and Outputs. You're essentially injecting your own Input (ngControl.control.valueChanges) and your own output (TOGGLE_EVENT).

Doesn't this *force* you to use ngModel? Meaning, how could you then use your widget outside of an ngModel context?

Reply to this Comment

@Ben,

You mean like this: https://plnkr.co/edit/pA1xY7Vq0GfylcRvs4m4 ? But this way component will not be even compatible with ngModel.

> But, in your example, you are basically are forcing the target component (Toggler) to know about both ngControl and the TOGGLE_EVENT.

True. But they are intended to work together anyway, so what we lose? And anyway, component still doesn't need to know, where they are coming from. It can be some crazy custom model binder that implements NgModel abstract class in its own unique way and your component will still be able to work with it. Consider those dependencies as just yet another type of contract in place of inputs-outputs. We're just outsourcing outer binding to ngModel instead of providing our own properties directly. And as I pointed out it gives us some advantages like validations and correct state tracking in an easy way.

>It seems like you are basically throwing out a number of the things that Angular2 gives you in terms of Inputs and Outputs.

Well, looks like trying to implement a bridge between component and ngModel without component knowing about it throws out even more things that Angular2 gives you with ngModel. :)

You can try the other way around - inject component into value accessor and control it from there. But I still don't see a point. Why? Keeping component "pure"? It is not the goal itself. The goal is to make a component which does this and that in a certain way. And there will always be some contract about how its inputs and outputs are bound to the outer world.

Reply to this Comment

@Ben,

> Doesn't this *force* you to use ngModel? Meaning, how could you then use your widget outside of an ngModel context?

As I said you can make them @Optional(). This way if you don't provide ngModel outer binding then it will just not be injected in your component. In the component, all you need is to check parameters for null before using them. This way you provide optional support for ngModel but not forcing user to actually use it. Combining with providing your own property-event pair it can give you both options - working with and without ngModel.

But here comes another question - are you really sure you need exactly ngModel? :) What about ngControl for example? Or if you don't provide ngModel support, how is your component supposed to be bound to the outer world then?

Reply to this Comment

I just found this answer on stackoverflow-- instead of trying to run another 'ngOnChanges' cycle programmatically, can you trigger an input change event?

My directive constructor takes in an ElementRef (the input is on there) and a Renderer (the preferred way to update the DOM in angular, if you have to). Here's the code to trigger the change event on that input:

let event: Event = new Event('change');
event.target = this.elementRef.nativeElement;

this.renderer.invokeElementMethod(this.elementRef.nativeElement, 'dispatchEvent', [event]);

This works beautifully for my needs. We're currently using Angular 2.0.0-beta.8

Reply to this Comment

@Alex,

I see some of your points. But, something about it still feels like there is an over-coupling of concerns. To me, it seems like the "Angular" intent is to create components through Inputs and Outputs. My approach is trying to keep with this intent and then build on top of it.

Plus, as I was saying earlier, you might also be dealing with a component that you cannot easily extend (such as a third-party component that doesn't implement ngModel). In that case, you would need to bridge the gap.

> Well, looks like trying to implement a bridge between component
> and ngModel without component knowing about it throws out even
> more things that Angular2 gives you with ngModel.

See, I view it as just the opposite - I view this approach as providing the ngModel contract "on top of" the target component. Not, throwing anything out. But, to your point, my selectors only provide for [ngModel]. You could / would probably have to update the selector to account for the other form control directives (which is what the native value accessors are doing under the hood, if I recall correctly). But, to be honest, I don't really know all that much about building template-driven forms, in this sense - it's something I need to get better about.

At this point, we might just be in disagreement as to the "way" things should be done :D But, I am definitely inspired by your ideas.

Reply to this Comment

@Mike,

Very interesting! I've not seen that approach before. I don't know off hand if that would do all the things that the Inputs are supposed to do. I guess it depends on what features you need to do. Natively, when an input changes, it also interplays with things like OnPush change detection. So, if you're using a non-default change detection strategy, you'd have to check to see if this event-based approach would actually signal change detection (and then propagate changes down to the View template, for example).

Very curious!

Reply to this Comment

@Ben,

> To me, it seems like the "Angular" intent is to create components through Inputs and Outputs.

Absolutely, we're disagree on that. :)

Let's speculate a bit. What actually makes it possible to bind component using ngModel? It is value accessor. This is hard fact. Take a look at NgModel directive's source (or others - they are absolutely the same in this regard). It calls selectValueAccessor() common function which expects to get at least one value accessor. So no value accessor = exception = no binding possible. You absolutely cannot bind with ngModel-like directives anything that doesn't have value accessor. Period.

And next thing to consider is - what for is value accessor? It is there to modify *DOM element* in respond to programmatic changes and wrap user input events so that proper state/data changes would get triggered on NgControl. This is "frontend" to DOM inside all that chain of controls and directives. This is primary source of events to NgControl and final call handler from it for actual updating of view.

But here is the trick. Let's say, we have input with ngModel and value accessor bound directly on it. In this case value accessor can directly catch events on input element and provide NgModel with input from user. And angular provides default one for us. This is quite simple and straightforward.

Now let's say we have another component which is not an input. We want to bind it with ngModel. Component should respond to changes somewhere inside its template, it may be just another component which is not an input as well. In general it is something somewhere inside, VA doesn't (and actually shouldn't) know where and what, but component itself of course does. In this case VA doesn't have element on which it could catch events, and most likely it doesn't even have element to update. What should it do then? How should it provide those events to NgModel it is intended to work with? So someone should provide those events somehow to value accessor. And this is where DI comes to play. It allows us to inject those external events into value accessor and allows value accessor not to care where they are actually coming from. VA just knows that when someone outside fires this event it means that change has happend (or element has been touched). That's all its concern. It doesn't care where and why it happend and who caught it and how. Someone who knows should take care of providing those events and firing them. And in all that stuff inputs and outputs does not help us a lot.

As I see it, inputs and outputs are for custom things which are not actually intended to be "main model for editing by this component". It may be, say, some settings provided to directive, or some special events it generates. Something secondary in general.

Reply to this Comment

@Alex,

But, I think you're making a false assumption that you can only interact with some controls via ngModel. This is not true. Even with Inputs, Textareas, Select elements, etc. - you can still always fallback to binding to properties and events if you want to:

<input [value]="someValue" (input)="handleEvent( $event )" />

ngModel doesn't enable communication with the target element - it simply facilitates a certain *kind* of data workflow.

Reply to this Comment

@Ben,

Not really. I don't make such an assumption. The point is that if your control supports binding via ngModel then you're free to use it or not depending on your needs. But if you need such a support (and in my case I do) and component does not provide it - then you have a problem. :)

Reply to this Comment

@Alex,

Ha ha, and I'm saying you don't have a problem because the ngModel implementation can be external to the target component :P

Ok, now we're just going around in circles.

Reply to this Comment

@Ben,

Agree, we're circling. :) But still disagree on the solution. I think you're trying to solve wrong problem, and it produces solution that is way too complicated to be universal, reliable and usable, but that's just my opinion and it can be wrong. May be it's worth to look at situation from different angle. I don't know.

Good luck! :)

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.