Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 4 ControlValueAccessor value on submit and value changes events

Inside my Angular (4 + Typescript) project I used already prepared component (it doesn't really matter which since the way of extending ControlValueAccessor is same, but if this matters I used ng2-select) which I wanted because of better UX and nicer look. After including it to my form component which is using reactive forms I saw, that on submit I get back (for the form control) object based on class SelectItem which is used inside prepared component, to manage selected item(s) - like this:

// console.log(this.myForm.values)
Object {
    txtInput: 'value',
    // ...
    preparedComponent: SelectItem { // <--
        id: '1',
        text: 'Chosen item'
    }
}

Since the component is basically just a replacement for standard select control, I was expecting that I'll get back an ID of chosen item formed as string - like this:

// console.log(this.myForm.values)
Object {
    txtInput: 'value',
    // ...
    preparedComponent: '1' // <--
}

After I learned what actually ControlValueAccessor is and how it works, I changed the way of what comes back on submit (or better to say on item change - onChange) to the ID of an item. The problem was solved for some time until I started setting predefined values (on editing instance) to form controls instead of empty values (on adding instance). I figured out that this time the writeValue is assigning values to form control based on what it is sent to it as predefined value. So in case that I sent predefined value as ...

Object {
    id: '1',
    text: 'Chosen item'
}

... this was also the value in Object on submit just as in the first code example above. I made a workaround for this, to assign correct value to control with onChange right after the writeValue is called. This was really improper way of doing it, but I couldn't figured out anything other than that:

public writeValue(val: any): void {
    this.selectedItems = val; // val is for example Object { id: '1', text: 'Chosen item' }

    setTimeout(() => {
        this.onChange(val.id); // just an example of emitted value, there should be some checks val is object and id exists in real code
    }, 0);
}

And that also worked as far as I didn't start with subscribe to form control's valueChanges. The problem is that with each assigning of predefined value to prepared component, there is also an emit of the value used after submit, back to subscribers even when using { emitEvent: false }.

So this are the things I want to have handled with prepared component somehow, since I want to get right value from control on submit and I don't want emit change to subscribers until the value is really not changed, not just replaced as predefined value. Any idea how to handle this will be appreciated.

like image 653
user1257255 Avatar asked Apr 20 '17 09:04

user1257255


People also ask

How do you use control value accessor?

onChange - the callback function to register on UI change. onTouch - the callback function to register on element touch. set value(val: any) - sets the value used by the ngModel of the element. writeValue(obj: any) - This will write the value to the view if the value changes occur on the model programmatically.

What is writeValue Angular?

Here are the methods of this interface, and how they work: writeValue: this method is called by the Forms module to write a value into a form control. registerOnChange: When a form value changes due to user input, we need to report the value back to the parent form.

What is Valueaccessor?

Control Value Accessor interface gives us the power to leverage the Angular forms API, and create a connection between it and the DOM element. The major benefits we gain from doing that, is that we get all the default validations you'd get with any element, in order to track the validity, and it's value.

What is ngDefaultControl in Angular?

Third party controls require a ControlValueAccessor to function with angular forms. Many of them, like Polymer's <paper-input> , behave like the <input> native element and thus can use the DefaultValueAccessor . Adding an ngDefaultControl attribute will allow them to use that directive.


1 Answers

I'm not sure I understood this correctly, but I'd like to give it a shot.

You want the component's input to be of the following type:

{
    id: string,
    text: string
}   

But you want the component's output to be just the string ID of the selected item.

I would argue that this data flow is a bit awkward. During initialization, preparedComponent is an object, but when an item is selected you want to turn it into a string. Aside from the issue of type safety, it just feels unintuitive.

For comparison, imagine doing this with ngModel and a text input:

<input type="text" [(ngModel)]="preparedComponent" />

Would you expect to pass in an object, and get back a string when the user types? Probably not.


But let's assume that it must be like this for some reason. You proceeded by either modifying ng2-select's implementation of ControlValueAccessor, or creating your own component which implements ControlValueAccessor and wraps ng2-select. (I can't tell which of these you were doing in your question). Now you call this.onChange(val.id) when an item is selected, which updates the parent component with the string ID.

Then you noticed a separate issue, which is that ng2-select fires a change event during initialization if you predefine a selected value. This is odd because the user did not interact with the select control yet. So you tried initializing preparedComponent by calling setValue on your form control with emitEvent: false, but I am guessing this didn't work because while it may have prevented a change event from being emitted when you initialized your data, ng2-select made the second change to your data immediately.

So instead, you worked around this by waiting until the first update by ng2-select updates the parent component to a new object, and then (using setTimeout) you overwrite it with the string ID you actually want. This keeps preparedComponentup to date with a string, but does not solve the issue of the change event firing even when the user did not interact with the control. (By the way, if you are wrapping ng2-select with your own component then I'm not sure why you'd have to do this setTimeout part, because wouldn't you already have another function which calls this.onChange(val.id) when ng2-select informs you of a new selected item, as I mentioned in the previous paragraph?)


Since I can't tell whether you are modifying ng2-select directly, or wrapping it with your own component:

If you are modifying ng2-select, why not just find its writeValue method and remove the code that emits an event? This way it updates the DOM when you predefine a value, but doesn't then overwrite your value.

If you are wrapping ng2-select with your own component, why not just modify your writeValue method to store a flag if ng2-select is about to fire an event, so you can ignore it:

private: ignoreSelectedEvent = false;

public writeValue(val: any): void {
    this.ignoreSelectedEvent = true;

    // Update ng2-select, which will cause an event to fire
}

And then in your function that reacts to a new selected item:

if (this.ignoreSelectedEvent) {
    this.ignoreSelectedEvent = false;

    return;
}

// Update the model with the string ID

this.onChange(val.id);
like image 54
Frank Modica Avatar answered Sep 28 '22 08:09

Frank Modica