Skip to content

Commit

Permalink
Merge pull request #441 from ng-select/sanitize-options
Browse files Browse the repository at this point in the history
Sanitize options
  • Loading branch information
varnastadeus committed Apr 11, 2018
2 parents a7d0fbd + 2b84190 commit 0801b0d
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 44 deletions.
4 changes: 2 additions & 2 deletions demo/app/examples/custom-templates.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { distinctUntilChanged, debounceTime, switchMap } from 'rxjs/operators'
<div *ngIf="item.name === 'Kaunas'">{{item.name}}</div>
<div class="card" *ngIf="item.name !== 'Kaunas'">
<div class="card-body">
<h5 class="card-title" [innerHTML]="item.name" [ngOptionHighlight]="search"></h5>
<h5 class="card-title" [ngOptionHighlight]="search">{{item.name}}</h5>
<h6 class="card-subtitle mb-2 text-muted">Card subtitle</h6>
<p class="card-text">
<img height="15" width="15" [src]="item.avatar"/>
Expand Down Expand Up @@ -59,7 +59,7 @@ import { distinctUntilChanged, debounceTime, switchMap } from 'rxjs/operators'
City group logo <img height="15" width="15" [src]="item.avatar"/>
</ng-template>
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
<b [innerHTML]="item.name" [ngOptionHighlight]="search"></b>
<b [ngOptionHighlight]="search">{{item.name}}</b>
</ng-template>
</ng-select>
---
Expand Down
8 changes: 7 additions & 1 deletion demo/app/examples/data-source.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,13 @@ import { Observable } from 'rxjs/Observable';
<p>
If you have simple use case, you can omit items array and bind options directly in html using <b>ng-option</b> component.
</p>
<button type="button" class="btn btn-secondary btn-sm" (click)="disable = !disable">Toggle disabled</button>
<button type="button" class="btn btn-secondary btn-sm" (click)="toggleDisabled()">Toggle disabled</button>
<hr/>
---html,true
<ng-select [searchable]="false" [(ngModel)]="selectedCarId">
<ng-option *ngFor="let car of cars" [value]="car.id" [disabled]="car.disabled" >{{car.name}}</ng-option>
<ng-option [value]="'custom'">Custom</ng-option>
<ng-option [value]="'html'"><b>BMW</b></ng-option>
</ng-select>
---
<br />Selected car ID: {{selectedCarId | json}}
Expand Down Expand Up @@ -101,6 +102,11 @@ export class DataSourceComponent {
this.dataService.getPeople().subscribe(items => this.people = items);
this.simpleItems = [true, 'Two', 3];
}

toggleDisabled() {
const car: any = this.cars[1];
car.disabled = !car.disabled;
}
}


4 changes: 2 additions & 2 deletions demo/app/examples/reactive-forms.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ import { delay } from 'rxjs/operators';
[virtualScroll]="true"
formControlName="album">
<ng-template ng-option-tmp let-item="item" let-search="searchTerm">
<div><span>Title: </span><span [innerHTML]="item.title" [ngOptionHighlight]="search"></span></div>
<div><span>Title: </span><span [ngOptionHighlight]="search">{{item.title}}</span></div>
<small><b>Id:</b> {{item.id}} | <b>UserId:</b> {{item.userId}}</small>
</ng-template>
</ng-select>
Expand Down Expand Up @@ -114,7 +114,7 @@ import { delay } from 'rxjs/operators';
</ng-template>
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
<img height="15" width="15" [src]="item.thumbnailUrl"/>
<span [innerHTML]="item.title" [ngOptionHighlight]="search"></span>
<span [ngOptionHighlight]="search">{{item.title}}</span>
</ng-template>
</ng-select>
<small class="form-text text-muted">5000 items with virtual scroll</small>
Expand Down
35 changes: 16 additions & 19 deletions src/ng-select/ng-option-highlight.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,44 @@ import { By } from '@angular/platform-browser';

@Component({
template: `
<span id="test1" [innerHTML]="'My text is highlighted'" [ngOptionHighlight]="'is high'"></span>
<span id="test2" [innerHTML]="'My text is not highlighted'" [ngOptionHighlight]="'test'"></span>
<span id="test3" [innerHTML]="'My text is rich'"></span>
<span id="test1" [innerHtml]="'My text is highlighted'" [ngOptionHighlight]="term"></span>
<span id="test2" [innerHtml]="'My text is not highlighted'" [ngOptionHighlight]="term"></span>
`
})
class TestComponent {
term: string;
}

describe('NgOptionHighlightDirective', function () {
describe('NgOptionHighlightDirective', () => {

let fixture: ComponentFixture<TestComponent>;

beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [NgOptionHighlightDirective, TestComponent]
})
.createComponent(TestComponent);
}).createComponent(TestComponent);

fixture.detectChanges();
});

it('should have two elements with highlight directive', () => {
let highlightDirectives = fixture.debugElement.queryAll(By.directive(NgOptionHighlightDirective));
const highlightDirectives = fixture.debugElement.queryAll(By.directive(NgOptionHighlightDirective));
expect(highlightDirectives.length).toBe(2);
});

it('should have one element with highlighted text when term matching', () => {
let span1 = fixture.debugElement.query(By.css('#test1'));
expect(span1.nativeElement.querySelector('.highlighted').innerHTML).toBe('is high');
expect(span1.nativeElement.textContent).toBe('My text is highlighted');
const span = fixture.debugElement.query(By.css('#test1'));
fixture.componentInstance.term = 'is high';
fixture.detectChanges();
expect(span.nativeElement.querySelector('.highlighted').innerHTML).toBe('is high');
expect(span.nativeElement.textContent).toBe('My text is highlighted');
});

it('should have one element with no highlighted text when term not matching', () => {
let span2 = fixture.debugElement.query(By.css('#test2'));
expect(span2.nativeElement.querySelector('.highlighted')).toBeNull();
expect(span2.nativeElement.innerHTML).toBe('My text is not highlighted');
});

it('should have one element with no highlighted text when no highlight directive', () => {
let span3 = fixture.debugElement.query(By.css('#test3'));
expect(span3.nativeElement.querySelector('.highlighted')).toBeNull();
expect(span3.nativeElement.innerHTML).toBe('My text is rich');
const span = fixture.debugElement.query(By.css('#test2'));
fixture.componentInstance.term = 'non matching';
fixture.detectChanges();
expect(span.nativeElement.querySelector('.highlighted')).toBeNull();
expect(span.nativeElement.innerHTML).toBe('My text is not highlighted');
});
});
39 changes: 27 additions & 12 deletions src/ng-select/ng-option-highlight.directive.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,62 @@
import * as searchHelper from './search-helper';
import {
AfterViewInit,
Directive,
ElementRef,
Input,
OnChanges,
Renderer2
Renderer2,
SimpleChanges
} from '@angular/core';
import { isDefined } from './value-utils';

@Directive({
selector: '[ngOptionHighlight]'
})
export class NgOptionHighlightDirective implements OnChanges {
export class NgOptionHighlightDirective implements OnChanges, AfterViewInit {

@Input('ngOptionHighlight') term: string;
@Input('innerHTML') label: any;

constructor(private elementRef: ElementRef, private renderer: Renderer2) { }
private element: HTMLElement;
private label: string;

ngOnChanges(): void {
this._highlightLabelWithSearchTerm();
constructor(
private elementRef: ElementRef,
private renderer: Renderer2) {
this.element = this.elementRef.nativeElement;
}

private _highlightLabelWithSearchTerm(): void {
const label: string = this.label ? this.label.toString() : '';
if (!label || !this.term) {
ngOnChanges(changes: SimpleChanges) {
if (isDefined(changes.term.currentValue) && isDefined(this.label)) {
this._highlightLabelWithSearchTerm();
}
}

ngAfterViewInit() {
this.label = this.element.innerHTML;
}

private _highlightLabelWithSearchTerm() {
const label = this.label;
if (!this.term) {
this._setInnerHtml(label);
return;
}

const indexOfTerm = searchHelper.stripSpecialChars(label)
.toLowerCase()
.indexOf(searchHelper.stripSpecialChars(this.term).toLowerCase());
if (indexOfTerm > -1) {
this._setInnerHtml(
label.substring(0, indexOfTerm)
+ '<span class=\'highlighted\'>' + label.substr(indexOfTerm, this.term.length) + '</span>'
+ `<span class="highlighted">${label.substr(indexOfTerm, this.term.length)}</span>`
+ label.substring(indexOfTerm + this.term.length, label.length));
} else {
this._setInnerHtml(label);
}
}

private _setInnerHtml(html): void {
private _setInnerHtml(html) {
this.renderer.setProperty(this.elementRef.nativeElement, 'innerHTML', html);
}
}
16 changes: 8 additions & 8 deletions src/ng-select/ng-select.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<div [class.disabled]="item.disabled" class="ng-value" *ngFor="let item of selectedItems">
<ng-template #defaultLabelTemplate>
<span class="ng-value-icon left" (click)="unselect(item); $event.stopPropagation()" aria-hidden="true">×</span>
<span class="ng-value-label" [innerHTML]="item.label"></span>
<span class="ng-value-label" [innerHtml]="item.label"></span>
</ng-template>

<ng-template
Expand Down Expand Up @@ -79,11 +79,11 @@
id="{{item?.htmlId || null}}">

<ng-template #defaultOptionTemplate>
<span class="ng-option-label" [innerHTML]="item.label" [ngOptionHighlight]="filterValue"></span>
<span class="ng-option-label" [innerHtml]="item.label"></span>
</ng-template>

<ng-template #defaultOptGroupTemplate>
<span class="ng-option-label" [innerHTML]="item.label" [ngOptionHighlight]="filterValue"></span>
<span class="ng-option-label" [innerHtml]="item.label"></span>
</ng-template>

<ng-template
Expand All @@ -98,19 +98,19 @@
</ng-container>

<ng-container *ngIf="showNoItemsFound()">
<ng-template #defaultNotfoundTemplate>
<div class="ng-option disabled" [innerHTML]="notFoundText" *ngIf="showNoItemsFound()"></div>
<ng-template #defaultNotFoundTemplate>
<div class="ng-option disabled">{{notFoundText}}</div>
</ng-template>

<ng-template
[ngTemplateOutlet]="notFoundTemplate || defaultNotfoundTemplate"
[ngTemplateOutlet]="notFoundTemplate || defaultNotFoundTemplate"
[ngTemplateOutletContext]="{ searchTerm: filterValue }">
</ng-template>
</ng-container>

<ng-container *ngIf="showTypeToSearch()">
<ng-template #defaultTypeToSearchTemplate>
<div class="ng-option disabled" [innerHTML]="typeToSearchText"></div>
<div class="ng-option disabled">{{typeToSearchText}}</div>
</ng-template>

<ng-template
Expand All @@ -120,7 +120,7 @@

<ng-container *ngIf="isLoading && itemsList.filteredItems.length === 0">
<ng-template #defaultLoadingTextTemplate>
<div class="ng-option disabled" [innerHTML]="loadingText"></div>
<div class="ng-option disabled">{{loadingText}}</div>
</ng-template>

<ng-template
Expand Down

0 comments on commit 0801b0d

Please sign in to comment.