Last active
December 8, 2023 16:15
-
-
Save jean-merelis/44a3ed842c24e99d38cd2f1e9249c473 to your computer and use it in GitHub Desktop.
Directive that provides support for MatFormFieldControl (Angular Material) in NgSelect.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// WIP | |
@import '~@angular/material/theming'; | |
@mixin ng-select-theme($theme) { | |
$primary: map-get($theme, primary); | |
$accent: map-get($theme, accent); | |
$warn: map-get($theme, warn); | |
$isdark: map-get($theme, is-dark); | |
$foreground: map-get($theme, foreground); | |
$background: map-get($theme, background); | |
$highlight-color: if($isdark, mat-color($foreground, text) ,mat-color($primary)); | |
.ng-select, .ng-select-container, .ng-input>input { | |
color: mat-color($foreground, text) !important; | |
font: inherit; | |
font-family: inherit; | |
} | |
.ng-placeholder{ | |
display: none; | |
} | |
.ng-clear-wrapper, .ng-arrow-wrapper{ | |
height: 1em; | |
color: mat-color($foreground, text, .4); | |
} | |
.ng-clear-wrapper:hover, .ng-arrow-wrapper:hover{ | |
color: mat-color($foreground, text); | |
} | |
.ng-select .ng-arrow-wrapper .ng-arrow{ | |
border-left: 5px solid transparent; | |
border-right: 5px solid transparent; | |
border-top: 5px solid; | |
height: 7px !important; | |
} | |
.ng-select.ng-select-single .ng-select-container .ng-value-container { | |
height: 1em; | |
} | |
.ng-select.ng-select-multiple{ | |
.ng-value{ | |
// WIP | |
color: mat-color($primary, default-contrast); | |
background: mat-color($primary); | |
padding: 2px 8px; | |
border-radius: 12px; | |
margin: 0 4px 2px 0; | |
.ng-value-label{ | |
margin-left: 8px; | |
} | |
} | |
} | |
.ng-dropdown-panel{ | |
@include mat-elevation(4); | |
background: mat-color($background, card); | |
color: mat-color($foreground, text) !important; | |
.mat-option.ng-option-selected:not(.ng-option-marked):not(:hover) { | |
background: mat-color($background, card); | |
&:not(.ng-option-disabled) { | |
color: mat-color($foreground, text); | |
} | |
} | |
// left: 0; | |
&.ng-select-bottom { | |
top: calc(100% + .9em ); | |
} | |
&.ng-select-top { | |
bottom: calc(100% + 1.25em); | |
} | |
&.multiple { | |
.ng-option { | |
&.selected { | |
background: mat-color($background,card); | |
} | |
&.marked { | |
background: mat-color($foreground, text, .04); | |
} | |
} | |
} | |
.ng-dropdown-header { | |
border-bottom: 1px solid mat-color($foreground, text,.12); | |
padding: 0 16px; | |
line-height: 3em; | |
min-height: 3em; | |
} | |
.ng-dropdown-footer { | |
border-top: 1px solid mat-color($foreground, text,.12); | |
padding: 0 16px; | |
line-height: 3em; | |
min-height: 3em; | |
} | |
.ng-dropdown-panel-items { | |
.ng-optgroup { | |
user-select: none; | |
cursor: pointer; | |
line-height: 3em; | |
height: 3em; | |
padding: 0 16px; | |
color: mat-color($foreground, text); | |
font-weight: 500; | |
&.ng-option-marked { | |
background:mat-color($foreground, text, .04); | |
} | |
&.ng-option-disabled { | |
cursor: default; | |
} | |
&.ng-option-selected { | |
background: mat-color($foreground, text, .12); | |
color: $highlight-color; | |
} | |
} | |
.ng-option { | |
line-height: 3em; | |
min-height: 3em; | |
white-space: nowrap; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
padding: 0 16px; | |
text-decoration: none; | |
position: relative; | |
color: mat-color($foreground, text,.87); | |
text-align: left; | |
&.ng-option-marked { | |
background: mat-color($foreground, text, .04); | |
color: mat-color($foreground, text,.87); | |
} | |
&.ng-option-selected { | |
background: mat-color($foreground, text, .12); | |
color: $highlight-color; | |
} | |
&.ng-option-disabled { | |
color: mat-color($foreground, text, 0.38); | |
} | |
&.ng-option-child { | |
padding-left: 32px; | |
} | |
.ng-tag-label { | |
padding-right: 5px; | |
font-size: 80%; | |
font-weight: 400; | |
color: mat-color($foreground, text, 0.38); | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<mat-form-field> | |
<ng-select placeholder="Select" ngSelectMat [items]="simpleItems" [(ngModel)]="selectedSimpleItem" required="true" #model="ngModel"></ng-select> | |
<mat-error *ngIf="model.invalid">Required</mat-error> | |
</mat-form-field> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { MatFormFieldControl } from '@angular/material/form-field'; | |
import { Directive, HostBinding, Input, Optional, Self, OnDestroy, DoCheck } from '@angular/core'; | |
import { Subject } from 'rxjs'; | |
import { NgControl, FormControl, FormGroupDirective, NgForm } from '@angular/forms'; | |
import { NgSelectComponent } from '@ng-select/ng-select'; | |
import { coerceBooleanProperty } from '@angular/cdk/coercion'; | |
import { untilDestroyed } from 'ngx-take-until-destroy'; | |
import { ErrorStateMatcher } from '@angular/material/core'; | |
export class NgSelectErrorStateMatcher { | |
constructor(private ngSelect: NgSelectFormFieldControlDirective) { | |
} | |
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
if (!control) { | |
return this.ngSelect.required && this.ngSelect.empty; | |
} else { | |
return !!(control && control.invalid && (control.touched || (form && form.submitted))); | |
} | |
} | |
} | |
@Directive({ | |
// tslint:disable-next-line:directive-selector | |
selector: '[ngSelectMat]', | |
providers: [{ provide: MatFormFieldControl, useExisting: NgSelectFormFieldControlDirective }], | |
}) | |
export class NgSelectFormFieldControlDirective implements MatFormFieldControl<any>, OnDestroy, DoCheck { | |
static nextId = 0; | |
@HostBinding() @Input() id = `ng-select-${NgSelectFormFieldControlDirective.nextId++}`; | |
@HostBinding('attr.aria-describedby') describedBy = ''; | |
get empty(): boolean { | |
return this._value === undefined || this._value === null; | |
} | |
errorState = false; | |
@Input() errorStateMatcher: ErrorStateMatcher; | |
private _defaultErrorStateMatcher: ErrorStateMatcher = new NgSelectErrorStateMatcher(this); | |
// controlType?: string; | |
// autofilled?: boolean; | |
stateChanges = new Subject<void>(); | |
focused = false; | |
get shouldLabelFloat() { return this.focused || !this.empty; } | |
@Input() | |
get placeholder(): string { return this._placeholder; } | |
set placeholder(value: string) { | |
this._placeholder = value; | |
this.stateChanges.next(); | |
} | |
private _placeholder: string; | |
@Input() | |
get required(): boolean { return this._required; } | |
set required(value: boolean) { | |
this._required = coerceBooleanProperty(value); | |
this.stateChanges.next(); | |
} | |
private _required = false; | |
@Input() | |
get disabled(): boolean { return this._disabled; } | |
set disabled(value: boolean) { | |
this._disabled = coerceBooleanProperty(value); | |
this.stateChanges.next(); | |
} | |
private _disabled = false; | |
@Input() | |
get value(): any { | |
return this._value; | |
} | |
set value(v: any) { | |
this._value = v; | |
this.stateChanges.next(); | |
} | |
private _value: any; | |
constructor( | |
private host: NgSelectComponent, | |
@Optional() @Self() public ngControl: NgControl, | |
@Optional() private _parentForm: NgForm, | |
@Optional() private _parentFormGroup: FormGroupDirective, | |
) { | |
host.focusEvent.asObservable().pipe(untilDestroyed(this)).subscribe(v => { | |
this.focused = true; | |
this.stateChanges.next(); | |
}); | |
host.blurEvent.asObservable().pipe(untilDestroyed(this)).subscribe(v => { | |
this.focused = false; | |
this.stateChanges.next(); | |
}); | |
if (this.ngControl) { | |
this._value = this.ngControl.value; | |
this._disabled = this.ngControl.disabled; | |
this.ngControl.statusChanges.pipe(untilDestroyed(this)).subscribe(s => { | |
const disabled = s === 'DISABLED'; | |
if (disabled !== this._disabled) { | |
this.disabled = disabled; | |
} | |
}); | |
this.ngControl.valueChanges.pipe(untilDestroyed(this)).subscribe(v => { | |
this._value = v; | |
this.host.detectChanges(); | |
this.stateChanges.next(); | |
}); | |
} else { | |
host.changeEvent.asObservable().pipe(untilDestroyed(this)).subscribe(v => { | |
this._value = v; | |
this.host.detectChanges(); | |
this.stateChanges.next(); | |
}); | |
} | |
} | |
ngOnDestroy() { } | |
ngDoCheck() { | |
// We need to re-evaluate this on every change detection cycle, because there are some | |
// error triggers that we can't subscribe to (e.g. parent form submissions). This means | |
// that whatever logic is in here has to be super lean or we risk destroying the performance. | |
this.updateErrorState(); | |
} | |
updateErrorState() { | |
const oldState = this.errorState; | |
const parent = this._parentFormGroup || this._parentForm; | |
const matcher = this.errorStateMatcher || this._defaultErrorStateMatcher; | |
const control = this.ngControl ? this.ngControl.control as FormControl : null; | |
const newState = matcher.isErrorState(control, parent); | |
if (newState !== oldState) { | |
this.errorState = newState; | |
this.stateChanges.next(); | |
} | |
} | |
setDescribedByIds(ids: string[]): void { | |
this.describedBy = ids.join(' '); | |
} | |
onContainerClick(event: MouseEvent): void { | |
const target = event.target as HTMLElement; | |
if (target.classList.contains('mat-form-field-infix')) { | |
this.host.focus(); | |
this.host.open(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
As I had issues with the approach above, I went to the source and did some minor modifications and got a usable result with minimal changes (all commented in the source). Now I'm able to use kendo angular controls with this directive inside mat-form-fields.