question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Not working with library input components that have implemented ControlValueAccessor

See original GitHub issue

I’m getting this error

error properties: Object({ originalStack: 'Error: No value accessor for form control with unspecified name attribute

when rendering an external library component that has ControlValueAccessor implemented. The library components structure is this

export class InputComponent extends InputBase<string> {
// and base 
export abstract class InputBase<T> implements ControlValueAccessor {

Component is used like this

<my-input
  #hexInput
  (input)="onKey(hexInput.value)"
  [(ngModel)]="currentColor"
  [label]="'HEX code'"
  [helperText]="'Please add a valid HEX color'"
  [invalid]="invalidHex"
></my-input>

I can fix the issue by adding ngDefaultControl attribute but afaik this issue occurs only because the component is not recognized as having implemented ControlValueAccessor.

Can you please assist with this?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:22 (12 by maintainers)

github_iconTop GitHub Comments

2reactions
Humberdcommented, Sep 11, 2020

Ok, so we managed to do a workaround in our app. We have around 1300 test files and manually adding ngDefaultControl wasn’t even an option.

Quick solution

import { Directive, NgModule } from '@angular/core';
import { DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Shallow } from 'shallow-render';

@Directive({
    // tslint:disable-next-line:directive-selector
    selector: '[formControl], [formControlName], [ngModel]',
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: Angular10ShallowCompatibilityDirective,
            multi: true,
        },
    ],
})
class Angular10ShallowCompatibilityDirective extends DefaultValueAccessor {}

@NgModule({
    declarations: [Angular10ShallowCompatibilityDirective],
    exports: [Angular10ShallowCompatibilityDirective],
})
class Angular10ShallowCompatibilityModdule {}

export function shallowSupportForAngular10(): void {
    Shallow.alwaysImport(Angular10ShallowCompatibilityModdule);
    Shallow.neverMock(Angular10ShallowCompatibilityDirective);
}

The shallowSupportForAngular10 should be added in the main test entry point. In our case it was setup-jest.ts

// setup-jest.ts
...
shallowSupportForAngular10();
...

Explanation

Let’s say we have the <sps-example-test-control> component that prints hello on screen and logs when it is created:

@Component({
    selector: 'sps-example-test-control',
    template: '<div>hello</div>',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExampleTestControlComponent {

    constructor() {
        console.log('Initiating example test control');
    }
}

Now let’s use our component in test component and attach [(ngModel)] to it:

@Component({
    selector: 'sps-example-test',
    template: `
        <sps-example-test-control name="age" [(ngModel)]="myValue"></sps-example-test-control>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExampleTestComponent implements AfterViewInit {
    myValue = 'xyz';

    @ViewChild(NgModel) ngModel: NgModel;

    constructor() {
        console.log('hello');
    }

    ngAfterViewInit(): void {
        console.log(this.ngModel);
    }
}

Angular 9

What we should be getting is an error, because our custom test control doesn’t implement ControlValueAccessor interface, which is required for ngModel to function properly, right?

When we run this component in our app as a normal component it behaves correctly, it throws an error:

Uncaught Error: No value accessor for form control with name: 'age'

However!!!, our test cases pass!!! What? Why? Shouldn’t the tests throw when view throws?

describe('Test Examples', () => {
    let shallow: Shallow<ExampleTestComponent>;

    beforeEach(
        async(() => {
            shallow = new Shallow(ExampleTestComponent, ExampleTestModule)
        })
    );

    it('should create', async () => {
        const { instance } = await shallow.render();
        expect(instance).toBeTruthy();
    });
});

It turns out when we print ngModel instance in our tests we get the result below. It seems that Shallow for some reason automatically created mock of our <sps-example-test-control> and automatically attached it to ngModel as valueAccessor. It should not work like this!

console.log src/app/views/sps-app/views/examples/example-test/components/example-test/example-test.component.ts:17
hello

console.log src/app/views/sps-app/views/examples/example-test/components/example-test/example-test.component.ts:21
NgModel {
  _parent: null,
  name: 'age',
  valueAccessor: MockOfExampleTestControlComponent { <---- It created a mock of our host component and treated it as valueAccessor, so ngModel didn't complain
    __simulateChange: [Function],
    __simulateTouch: [Function],
    writeValue: [Function],
    __hide: [Function],
    __render: [Function]
  },
  _rawValidators: [],

Angular 10

In Angular 10 it was fixed, that means the test throws an error, because Shallow didn’t automagically treated our mock as valueAccessor. So everything should be fine, right? 😃

It turns out that all our valueAccessor bindings are performed via provider inside @Component({providers: [{provide: NG_VALUE_ACCESSOR, ...}]}) decorator, BUT because Shallow mocks all of the controls, the provider is never instantiated and thus each test case that uses our custom control is broken 😦

1reaction
getsafcommented, Sep 18, 2020

Hell yeah! Glad this worked out. I had been meaning to tackle component-level providers for a while but it hadn’t given anyone any trouble until you mentioned it.

Thanks for reporting. I’ll get a proper 10.1.0 release out soon and update the docs.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Implementing controlValueAccessor in our external Angular ...
It looks like the only problem is using our Angular component library, and the host Angular application can't detect the library's ...
Read more >
Never again be confused when implementing ... - InDepth.Dev
Here I'll first explain why we need ControlValueAccessor and how it's used inside Angular. Then I'll demonstrate how to wrap a 3rd party...
Read more >
Create a custom form control component in Angular with ...
Introducing Control Value Accessor Interface! · You need to import either the Template Driven Forms or Reactive Forms modules in order to use...
Read more >
Creating a custom form control in Angular - Orjan De Smet
So we start by implementing the ControlValueAccessor interface from @angular/forms into the component. This interface has three functions that ...
Read more >
Angular Reactive Forms: Custom Form Control - Medium
First, component needs to implement ControlValueAccessor interface. · Second, component has to be added to the list of known value accesors. It ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found