Update 3
I worked through making my library component implement a CVA as suggested by @Ingo, but I discovered that since my component wraps @ng-select/ng-select, some of the requisite functions broke the basic functionality of ng-select. Namely, when selecting a value from the item list, it would populate the value but it would remain hidden. Removing all of the required CVA methods makes a working library component, but the challenge of surfacing the selected value in the calling form remains for me (still working on that).
Alternately, adding an Event Emitter and defining an @Output seemed to run into a similar situation where attempting to intercept the change event in the ng-select results in breaking it. It could very well be ng-select is a poor candidate for embedding in a library component.
Update 2
From an abundance of sources, it seems like using @Output and EventEmitter is the correct answer to this. If nobody else cares to offer an answer I will write one up when I have this all working and post then.
Original Question
I decided to start trying out the Angular 6 library feature but I've upgraded to the Angular 7 CLI. I don't think that matters but I'm pretty green yet with Angular development.
The component I want to make into a library is a form control built on ng-select. It receives the formGroup from the parent form component via the parent input.
<div [formGroup]="parent" _ngcontent-ikt-1 class="container">
<ng-select
[items]="peopleBuffer"
bindLabel="text"
bindValue="id"
[typeahead]="input$"
formControlName="assocNumber"
#select>
<ng-template ng-option-tmp let-item="item" let-search="searchTerm">
<span [ngOptionHighlight]="search">{{item?.text}}</span>
</ng-template>
</ng-select>
</div>
The .ts file for this has parent as an @Input()
@Input() parent: FormGroup;
And in the parent form component, I mount the control like this
<app-lookuplist _nghost-ikt-1
[parent]="form">
</app-lookuplist>
In the parent form .ts file, form is of type FormGroup and is constructed when @ngOnInit is called. In a self contained project, this works fine.
My big question is, though, what is the proper way to abstract this relationship?
Should I be using a schematic rather than a library? Or is there a proper way to expose this input so that this will build initially? I could put it into a test harness and provide that input like the self contained app, but at build time, it produces an error you're all probably very familiar with:
Can't bind to 'formGroup' since it isn't a known property of 'div'.
I followed several different tutorials which all seem to follow the same general structure.
https://blog.angularindepth.com/creating-a-library-in-angular-6-87799552e7e5
https://angular.io/guide/creating-libraries ... to name two I suspect the answer is here but my understanding is still in its infancy on this subject.
All help and instruction appreciated.
Update
The second link I provided above seems to offer the opinion that inputs should be stateless...
To make your solution reusable, you need to adjust it so that it does not depend on app-specific code. Here are some things to consider in migrating application functionality to a library.
Declarations such as components and pipes should be designed as stateless, meaning they don’t rely on or alter external variables.
(emphasis added). What I had been doing is making this input part of the form and then getting the values from the form on submit. It sounds rather like I should invert this so that the library component emits an event, as in this answer. The question remains if this is the best way. It seems more logical to handle the values of the library component via an event.
Ok, I think I figured it out. Or at least, I have a working solution that makes sense to me at my current level of understanding. If you see ways to optimize, please let me know.
Part of the problem was getting a ControlValueAccessor to work with ng-select without breaking its native functionality. Specifically, when you use ng-select as a type ahead control, and select a value, it should add a tag showing the values you have selected so far. Trying to implement the ControlValueAccessor interface broke this, possibly because it overwrote the interface onChange handler provided with ng-select.
So the path I took combined both approaches that I had seen employed and mentioned in the question. My ng-select implementation now lives in a nested component which is enclosed in a wrapper component that has the ControlValueAccessor on it.
This makes the following rough structure:
app.component (test harness)
people-search.component (ControlValueAccessor)
namelookup.component (@Outputs an EventEmitter)
Migrating the namelookup out of the original project then only needed a few small changes. I redacted the [formGroup]="parent" part from its html as this is no longer part of the context for the control.
In the namelookup.component.html, I added:
<ng-select
...
(change)="handleSelection($event)"
...>
In the namelookup.component.ts
@Component({
selector: 'ps-namelookup',
templateUrl: './namelookup.component.html',
styleUrls: ['./namelookup.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush, // new code
})
export class NameLookupComponent implements OnInit {
...
@Output() selectedValue = new EventEmitter<string>();
handleSelection(event) {
console.log('handleSelection in NameLookupComponent fired');
this.selectedValue.emit(event);
}
...
}
Then a new wrapper was created, people-search.component which had a basic ControlValueAccessor implemantion as outlined in many examples. The salient points of which are:
Provide an output hook in your template for the emitted event (selectedValue) from the child component in your CVA wrapper.
@Component({
selector: 'ps-people-search',
template: `
<ps-namelookup (selectedValue)="handleselectedvalue($event)"></ps-namelookup>
`,
...
Add a method to the class definition as well as implement CVA.
export class PeopleSearchComponent implements ControlValueAccessor {
....
private propagateChange = (_: any) => { };
handleselectedvalue($event) {
console.log('handleselectedvalue fired');
console.log($event);
this.data = $event[0].id;
this.propagateChange(this.data);
console.log('handling change event and propagating ' + this.data);
}
Not failing to also implement registerOnChange and registerOnTouched at minimum. The compiler may insist on a few others.
Finally, in the test harness app.component.html, to display the value on the form so that I know the selected value has been passed back up, I added:
{{form.value | json}}
And in the app.component.ts provided a stand-in FormGroup object.
public form: FormGroup;
constructor(private fb: FormBuilder){
console.log('constructing test harness appComponent');
this.form = this.fb.group({});
}
This is an answer that answers my root question, there may be other good ways to answer this. Feedback, optimizations and corrections certainly are welcome.