In A Unidirectional Data Flow, How Should A Selection Component Handle Incompatible Rendering Options?
When it comes to unidirectional / one-way data flow, I understand the idea that a component should never change data directly. Instead, it should alert the desired change to the calling context; and then, leave it up to the calling context to decide whether or not the desired change should be applied to the view-model. But, yesterday, I ran into a situation in which my mental model for this work-flow was unclear: what happens when a selection-component is given a value and a set of options and none of the provided options match the provided value? Should the selection-component alert the calling context that no match could be rendered? Or, should it just remain inactive until the selection is explicitly manipulated by the user?
To make this concrete, imagine a custom selection widget that takes a value and a set of options and can trigger an onChange() handler:
<custom-selection-widget
value="currentValue"
options="[ /* empty array */ ]"
onChange="handleChange( value )">
</custom-selection-widget>
In this case, we're passing the selection widget a value; but, for the selection options, we're passing-in an empty array. As such, when the selection widget renders, it will not be able to render an option that matches the provided value. As such, should it just render the empty-set of options? Or, should it also trigger the onChange() callback with something like a "null" value?
Another slant on this scenario would be a case in which the selection widget can render the provided value; but, at some point in the future, the selected option is deleted from the options list. This brings us back to a point in which the selection widget can no longer render the provided value. Again, should the selection widget just wait until an explicit user selection? Or, should it also trigger the onChange() callback with something like a "null" value?
Honestly, I could see this being argued in either direction. On one hand, a user interface control adds coincidental constraints to the user interactions. As such, you could argue that the selection-component should alert the calling context to the fact that the lack of compatible options creates a constraint that forces the value to be changed.
But, on the other hand, you could argue that the role of the a component in a unidirectional data flow is just to render the given data and respond to explicit user interactions. And, since the user didn't explicitly change the selection, the fact that there are no compatible options is irrelevant.
Frankly, the latter argument - that the selection-component does nothing - is easier to reason-about. And easier to code. Plus, the former argument - that the selection-component should trigger the onChange() handler - still doesn't get us out of the quandary because there's no guarantee that the calling context will react to the onChange() invocation. As such, even in the former argument, there's no guarantee that the selection-component will ever have a compatible value.
For these reasons, I am creating a mental model in which the select-component takes no action in the case where it has no option that is compatible with the provided value.
That said, I wanted to see how something like NgModel in Angular 7.1.3 would handle this. So, I created a tiny demo that mirrored my context:
// Import the core angular services.
import { Component } from "@angular/core";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
@Component({
selector: "my-app",
styleUrls: [ "./app.component.less" ],
template:
`
<select name="selectValue" [(ngModel)]="selectValue">
<!-- No options provided for control. -->
</select>
`
})
export class AppComponent {
public selectValue: string;
// I initialize the app component.
constructor() {
this.selectValue = "initial value";
// Let's see what Angular does to the value after the page has had a chance
// to render and the ngModel directive has synchronized the form values.
setTimeout(
() => {
console.group( "Select Value" );
console.log( this.selectValue );
console.groupEnd();
},
3000
);
}
}
As you can see, we're defining a "selectValue" for our NgModel Select-control; but, we're not providing any Options for the Select. We're then logging out the results of the "selectValue" to see if the NgModel directive changed it. And, when we run this code, we get the following output:
As you can see, the NgModel directive in Angular 7.1.3 matches my evolving mental model: even though the Select control can't render a compatible Option, it leaves the view-model as-is. It doesn't turn around and constrain the view-model to match the constraints of the widgetry.
I think this all makes sense. The role of a user interface (UI) component is to [try and] render the view-model and to capture user interactions. As such, the fact that a Selection component has no compatible rendering option is irrelevant with regard to the work-flow. Sure, it will affect the interactions that the user can initiate. But, the Selection component shouldn't initiate a work-flow on its own based on its own constraints.
Want to use code from this post? Check out the license.
Reader Comments
Hi Ben. I think your assumption is correct. The select value should output the value you give it, regardless of whether there are any matching options.
I actually have an issue that involves a form control. It is a different problem, but I wonder whether you could offer an opinion?
I am looping out several 'image card components' within a parent component. Each child 'image card component' contains a form with a single text input element [comments form]:
someParentComponent.html
someChildComponent.html
Comments section of child component:
My question is, in the child component '.ts' file, can I reference the same form name & input name, in each component, like:
someChildComponent.component.ts
Even, if there are several form group & form control names that are the same on the same page.
I was under the impression that each child component's variables were sandboxed? And that Angular would provide a unique reference under the hood?
But, I seem to be getting errors, when I try this? The error is telling me that I need to create a form group & form name reference in the component, which I have done. So, I wondered whether Angular was getting confused by several identical forms on the same page?
Or do I need to give each form a unique reference, like:
someChildComponent.html
Comments section of child component:
Apologies for asking an off topic question, but I cannot find anything on Google about this, and I knew you would be able to answer this easily!!!!
Sorry. The names of the 'html' pages, should be:
someChildComponent.component.html
someParentComponent.component.html
I just did some further research and it seems I should be using formControlName="someFormControlName", rather than formControl="someFormControlName", in my template.
Maybe this is why the error happens?
I get very confused by the difference between 'formControl' & 'formControlName'???
@Charles,
You get confused? Ha -- I get confused :D Are you using Reactive Forms? I've only used Template-driven forms so far. And, even then, I tend to forget that I need to add some name to some control elements. In other words, the solution to your issue doesn't immediately pop to mind. It sounds like you are on the right track though (with your last comment). You're already doing way more advanced form stuff than I am.
Hey Ben. When I changed formControl="someFormControl" to formControlName="someFormControlName", everything worked.
The strange thing is that in the corresponding component, I use:
And it works. So, I am converting the form control name in the template to a form control in the controller. I think I read somewhere that the form control name is just used as a string reference.
However, when I took a step back and looked at the bigger picture, I realised just how fantastic, Angular is.
Essentially, I can have an infinite number of child components, in this case, a number of material cards with a single image in each. These cards are looped out by the parent component. But the beauty is, that each of these child components have all their variables & directives sandboxed. The children are structural clones, but each behaves differently, depending on their API data.
It is so elegant, my mind is blown.
But there is more. Each child has a comments container which has an 'infinite scroller' directive, attached to it, but each works independently of the other, but with the same reference. I mean one might have 5 comments in it, and another might have 10, but the scroller just works, even though it is running in parallel.
Angular's power really shines, when you are building apps at scale, that have parent-child-grandchild components embedded. Trying to manage this, using Vanilla JS, is of course possible, using something like prototypal inheritance, but Angular makes this a breeze!
Wow. I am going off on one here...
But, thanks, once again, for your help anyway!
@Charles,
It is really cool stuff :D When I first started using AngularJS, I really only used
ng-controller
, which is like the baby-step into the sandboxing that you're talking about. It wasn't until our team started using ReactJS that I started to see how I could re-organize the AngularJS code to be much more safely sandboxed. And, now, with Angular 2+, the sandboxing is all just built-in as "the way" to architect the application. It's really cool - and really forces you to think about how data is being passed-around.Yes totally. I don't know much about this stuff, but I presume it's because, each set of variables is held within it's own class container. And each container is divided into M[model.ts]V[component.html]C[component.ts].
This allows us to scale applications seamlessly, whilst keeping business logic & semantics separate. Plus there is the convenience of being able to inject from services and inherit from parent components! An amazing system!