Skip to content

Commit 389b7a2

Browse files
author
esuau
committed
#72 Add search component
1 parent dad486a commit 389b7a2

File tree

5 files changed

+359
-0
lines changed

5 files changed

+359
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from "./pill-input/pill-input.module";
2121
export * from "./placeholder/placeholder.module";
2222
export * from "./progress-indicator/progress-indicator.module";
2323
export * from "./radio/radio.module";
24+
export * from "./search/search.module";
2425
export * from "./select/select.module";
2526
export * from "./switch/switch.module";
2627
export * from "./table/table.module";
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { ComponentFixture, TestBed } from "@angular/core/testing";
2+
import { By } from "@angular/platform-browser";
3+
4+
import { Search } from "./search.component";
5+
import { FormsModule } from "@angular/forms";
6+
7+
describe("Search", () => {
8+
let component: Search;
9+
let fixture: ComponentFixture<Search>;
10+
let inputElement: HTMLInputElement;
11+
let containerElement: HTMLElement;
12+
let clearButtonElement: HTMLButtonElement;
13+
14+
beforeEach(() => {
15+
TestBed.configureTestingModule({
16+
declarations: [Search],
17+
imports: [FormsModule],
18+
providers: []
19+
});
20+
});
21+
22+
beforeEach(() => {
23+
fixture = TestBed.createComponent(Search);
24+
component = fixture.componentInstance;
25+
inputElement = fixture.debugElement.query(By.css("input")).nativeElement;
26+
});
27+
28+
it("should work", () => {
29+
expect(component instanceof Search).toBe(true);
30+
});
31+
32+
it("should bind input value", () => {
33+
component.value = "Text";
34+
fixture.detectChanges();
35+
expect(inputElement.value).toEqual("Text");
36+
});
37+
38+
it("should bind input disabled", () => {
39+
expect(inputElement.disabled).toEqual(false);
40+
component.disabled = true;
41+
fixture.detectChanges();
42+
expect(inputElement.disabled).toEqual(true);
43+
});
44+
45+
it("should bind input required", () => {
46+
component.required = true;
47+
fixture.detectChanges();
48+
expect(inputElement.required).toEqual(true);
49+
});
50+
51+
it("should display component of the correct size", () => {
52+
containerElement = fixture.debugElement.query(By.css(".bx--search")).nativeElement;
53+
component.size = "lg";
54+
fixture.detectChanges();
55+
expect(containerElement.className.includes("bx--search--lg")).toEqual(true);
56+
component.size = "sm";
57+
fixture.detectChanges();
58+
expect(containerElement.className.includes("bx--search--sm")).toEqual(true);
59+
});
60+
61+
it("should display clear button", () => {
62+
clearButtonElement = fixture.debugElement.query(By.css("button")).nativeElement;
63+
component.value = "";
64+
fixture.detectChanges();
65+
expect(clearButtonElement.className.includes("bx--search-close--hidden")).toEqual(true);
66+
component.value = "Text";
67+
fixture.detectChanges();
68+
expect(clearButtonElement.className.includes("bx--search-close--hidden")).toEqual(false);
69+
});
70+
71+
it("should clear input when clear button is clicked", () => {
72+
component.value = "Text";
73+
fixture.detectChanges();
74+
clearButtonElement = fixture.debugElement.query(By.css("button")).nativeElement;
75+
clearButtonElement.click();
76+
fixture.detectChanges();
77+
expect(component.value).toEqual("");
78+
});
79+
});

src/search/search.component.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import {
2+
Component,
3+
Input,
4+
EventEmitter,
5+
Output
6+
} from "@angular/core";
7+
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from "@angular/forms";
8+
9+
/**
10+
* Used to emit changes performed on search components.
11+
* @export
12+
* @class SearchChange
13+
*/
14+
export class SearchChange {
15+
/**
16+
* Contains the `Search` that has been changed.
17+
* @type {Search}
18+
* @memberof SearchChange
19+
*/
20+
source: Search;
21+
/**
22+
* The value of the `Search` field encompassed in the `SearchChange` class.
23+
* @type {string}
24+
* @memberof SearchChange
25+
*/
26+
value: string;
27+
}
28+
29+
/**
30+
* @export
31+
* @class Search
32+
* @implements {ControlValueAccessor}
33+
*/
34+
@Component({
35+
selector: "ibm-search",
36+
template: `
37+
<div class="bx--form-item">
38+
<div
39+
class="bx--search"
40+
[ngClass]="{
41+
'bx--search--sm': size === 'sm',
42+
'bx--search--lg': size === 'lg'
43+
}"
44+
role="search">
45+
<label class="bx--label" [for]="id">Search</label>
46+
<input
47+
class="bx--search-input"
48+
type="text"
49+
role="search"
50+
[id]="id"
51+
[value]="value"
52+
[placeholder]="placeholder"
53+
[disabled]="disabled"
54+
[required]="required"
55+
[attr.aria-labelledby]="ariaLabelledby"
56+
(click)="onClick($event)"
57+
(change)="onChange($event)"
58+
(input)="onSearch($event.target.value)"/>
59+
<svg
60+
class="bx--search-magnifier"
61+
width="16"
62+
height="16"
63+
viewBox="0 0 16 16">
64+
<path
65+
d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm4.936-1.27l4.563 4.557-.707.708-4.563-4.558a6.5 6.5 0 1 1 .707-.707z"
66+
fill-rule="nonzero"/>
67+
</svg>
68+
<button
69+
class="bx--search-close"
70+
[ngClass]="{
71+
'bx--search-close--hidden': !value || value === 0
72+
}"
73+
title="Clear search input"
74+
[attr.aria-label]="ariaLabel"
75+
(click)="clearSearch()">
76+
<svg
77+
width="16"
78+
height="16"
79+
viewBox="0 0 16 16"
80+
xmlns="http://www.w3.org/2000/svg">
81+
<path
82+
d="M8 6.586L5.879 4.464 4.464 5.88 6.586 8l-2.122 2.121 1.415 1.415L8 9.414l2.121 2.122 1.415-1.415L9.414
83+
8l2.122-2.121-1.415-1.415L8 6.586zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"
84+
fill-rule="evenodd"/>
85+
</svg>
86+
</button>
87+
</div>
88+
</div>
89+
`,
90+
providers: [
91+
{
92+
provide: NG_VALUE_ACCESSOR,
93+
useExisting: Search,
94+
multi: true
95+
}
96+
]
97+
})
98+
export class Search implements ControlValueAccessor {
99+
/**
100+
* Variable used for creating unique ids for search components.
101+
*/
102+
static searchCount = 0;
103+
104+
/**
105+
* Size of the search field.
106+
*/
107+
@Input() size: "sm" | "lg" = "lg";
108+
/**
109+
* Set to `true` for a disabled search input.
110+
*/
111+
@Input() disabled = false;
112+
/**
113+
* Sets the name attribute on the `input` element.
114+
*/
115+
@Input() name: string;
116+
/**
117+
* The unique id for the search component.
118+
*/
119+
@Input() id = `search-${Search.searchCount}`;
120+
/**
121+
* Reflects the required attribute of the `input` element.
122+
*/
123+
@Input() required: boolean;
124+
/**
125+
* Sets the value attribute on the `input` element.
126+
*/
127+
@Input() value = "";
128+
/**
129+
* Sets the placeholder attribute on the `input` element.
130+
*/
131+
@Input() placeholder = "Search";
132+
/**
133+
* Used to set the `aria-label` attribute on the input element.
134+
*/
135+
// tslint:disable-next-line:no-input-rename
136+
@Input("aria-label") ariaLabel = "Clear search input";
137+
/**
138+
* Used to set the `aria-labelledby` attribute on the input element.
139+
*/
140+
// tslint:disable-next-line:no-input-rename
141+
@Input("aria-labelledby") ariaLabelledby: string;
142+
/**
143+
* Emits event notifying other classes when a change in state occurs in the input.
144+
*/
145+
@Output() change = new EventEmitter<SearchChange>();
146+
147+
/**
148+
* Creates an instance of `Search`.
149+
* @memberof Search
150+
*/
151+
constructor() {
152+
Search.searchCount++;
153+
}
154+
155+
// this is the initial value set to the component
156+
public writeValue(value: any) {
157+
this.value = value;
158+
}
159+
160+
/**
161+
* Sets a method in order to propagate changes back to the form.
162+
* @param {any} fn
163+
* @memberof Search
164+
*/
165+
public registerOnChange(fn: any) {
166+
this.propagateChange = fn;
167+
}
168+
169+
/**
170+
* Registers a callback to be triggered when the control has been touched.
171+
* @param fn Callback to be triggered when the search input is touched.
172+
*/
173+
public registerOnTouched(fn: any) {
174+
this.onTouched = fn;
175+
}
176+
177+
/**
178+
* Executes on the event of a change within `Search` to block propagation.
179+
* @param {any} event
180+
* @memberof Search
181+
*/
182+
onChange(event) {
183+
event.stopPropagation();
184+
}
185+
186+
/**
187+
* Handles click events on the `Search` and emits changes to other classes.
188+
* @param {any} event
189+
* @memberof Search
190+
*/
191+
onClick(event) {}
192+
193+
/**
194+
* Called when search input is blurred. Needed to properly implement `ControlValueAccessor`.
195+
* @memberof Search
196+
*/
197+
onTouched: () => any = () => {};
198+
199+
/**
200+
* Method set in `registerOnChange` to propagate changes back to the form.
201+
* @memberof Search
202+
*/
203+
propagateChange = (_: any) => {};
204+
205+
/**
206+
* Called when text is written in the input.
207+
* @param {string} search The input text.
208+
*/
209+
onSearch(search: string) {
210+
this.value = search;
211+
this.emitChangeEvent();
212+
}
213+
214+
/**
215+
* Called when clear button is clicked.
216+
* @memberof Search
217+
*/
218+
clearSearch(): void {
219+
this.value = "";
220+
}
221+
222+
/**
223+
* Creates a class of `RadioChange` to emit the change in the `RadioGroup`.
224+
*/
225+
emitChangeEvent() {
226+
let event = new SearchChange();
227+
event.source = this;
228+
event.value = this.value;
229+
this.change.emit(event);
230+
}
231+
}

src/search/search.module.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// modules
2+
import { NgModule } from "@angular/core";
3+
import { FormsModule } from "@angular/forms";
4+
import { CommonModule } from "@angular/common";
5+
6+
// imports
7+
import { Search } from "./search.component";
8+
9+
@NgModule({
10+
declarations: [
11+
Search
12+
],
13+
exports: [
14+
Search
15+
],
16+
imports: [
17+
FormsModule,
18+
CommonModule
19+
]
20+
})
21+
export class SearchModule { }
22+
23+
export { Search };

src/search/search.stories.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { storiesOf, moduleMetadata } from "@storybook/angular";
2+
import { withKnobs } from "@storybook/addon-knobs/angular";
3+
4+
import { SearchModule } from "../";
5+
6+
storiesOf("Search", module).addDecorator(
7+
moduleMetadata({
8+
imports: [SearchModule]
9+
})
10+
)
11+
.addDecorator(withKnobs)
12+
.add("Basic", () => ({
13+
template: `
14+
<div style="width: 250px;">
15+
<ibm-search [size]="'lg'"></ibm-search>
16+
</div>
17+
`
18+
}))
19+
.add("Small", () => ({
20+
template: `
21+
<div style="width: 250px;">
22+
<ibm-search [size]="'sm'"></ibm-search>
23+
</div>
24+
`
25+
}));

0 commit comments

Comments
 (0)