In my Angular project, I created a search box with a button to get a search result from another component. I have a router outlet in my App Component and I switch router outlet with the search result component using the search value variable. I use a service to share this search value variable between components. So, when I click on a link in html, the router outlet will appear. When I click on the search input and do a search, search result will appear. My problem is, when the router outlet is activated, I have to click twice on search button or hit twice enter key to appear the search result.
Code -
search.component.ts:
export class SearchComponent implements OnInit {
value: string;
constructor(private data: SendDataService){}
show: boolean = true;
showEl(){
this.show = true;
}
newValue() {
this.data.changeValue(this.value)
this.show = false;
}
ngOnInit(): void{
this.data.currentValue.subscribe(value => this.value = value)
}
}
search.component.html:
<input type="text" [(ngModel)]="value" (click)="showEl()" (keyup.enter)="newValue()" (input)="showEl()">
<button (click)="newValue()">Search</button>
search-result.component.ts:
export class SearchResultComponent implements OnInit {
_postsArray: = //JSON Object;
value: string = "";
filterarray: any[] = [];
constructor(private data: SendDataService){}
getData(){
this.data.currentValue.subscribe(value => {this.value = value;
this.showData();
})
}
showData(){
if (this.value != null){
this.filterarray=
this._postsArray.filter(f =>
f.title.toLowerCase()
.includes(this.value.toLowerCase()))
.map(searchname=>searchname.title)
}
}
ngOnInit(): void{
this.getData();
}
}
app.component.html:
<div>
<div *ngIf="!value">
<router-outlet></router-outlet>
</div>
<div *ngIf="value">
<app-search-result></app-search-result>
</div>
</div>
When I put {{value}} in app.component.html, it shows the value at the first click of search button. but <app-search-result> only appears in second click. How can I solve this?
Related
I am trying to create a reusable form editor component that shows/hides a toolbar at the bottom of the form based on if the form is dirty. The toolbar has a save button and a reset form button. Both of the buttons on the toolbar need to be controlled based on the validity of the form. I used this tutorial here which is great... but I want to be able to reuse the component by passing in any form.
Here is my code (notice I am creating a FormGroup and passing it directly to ContentEditorComponent as an Input. My custom dirtyCheck operator is at the bottom.)
content-editor.component.ts
#Component({
selector: 'content-editor',
template: `
<mat-card class="card">
<ng-content></ng-content>
<div class="footer" *ngIf="isDirty$ | async">
<button type="submit" [disabled]="form.invalid$">
</div>
</mat-card>
`,
})
export class ContentEditorComponent implements OnInit, OnDestroy
{
#Input('formGroup') form: FormGroup;
isDirty$: Observable<boolean>;
source: Observable<any>;
ngOnInit(): void {
this.source = of(cloneDeep(this.form.value));
this.isDirty$ = this.form.valueChanges.pipe(
dirtyCheck(this.source)
);
}
initForm(): void {
this.source = of(cloneDeep(this.form.value));
this.isDirty$ = this.form.valueChanges.pipe(
takeUntil(this.unsubscribeAll),
dirtyCheck(this.source)
);
}
reset(): void {
this.form.patchValue(this.source);
this.initForm();
}
}
service-area-editor.component.html
<content-editor [formGroup]="form">
... my form
</content-editor>
dirty-check.ts
export function dirtyCheck<U>(source: Observable<U>) {
let subscription: Subscription;
let isDirty = false;
return function <T>(valueChanges: Observable<T>): Observable<boolean> {
const isDirty$ = combineLatest([
source,
valueChanges,
]).pipe(
debounceTime(300),
map(([a, b]) => {
console.log(a, b);
return isDirty = isEqual(a, b) === false;
}),
finalize(() => this.subscription.unsubscribe()),
startWith(false)
);
subscription = fromEvent(window, 'beforeunload').subscribe(event => {
isDirty && (event.returnValue = false);
});
return isDirty$;
};
}
How do I get the dirtyCheck operator to work correctly when the form can be anything? I cant supply the dirtyCheck operator with a source stream because the source stream comes from the parent component.
I mocked up a very small example of my problem here: https://github.com/lovefamilychildrenhappiness/AngularCustomComponentValidation
I have a custom component, which encapsulates an input field. The formControl associated with this input field has Validators.required (it is a required field). Inside the custom component, I have an onChange event which is fired when text is entered. I check if field is empty; if so, I add css class using ngClass. I also have set the registerOnChange of NG_VALUE_ACCESSOR, so I notify the form when the input changes. Finally, I implement NG_VALIDATORS interface to make the formControl invalid or valid.
My problem is I have a button that is clicked (it's not the submit button). When this button is clicked, I need to check if the custom component is blank or not, and if it is, change the css class and make the form invalid. I think the validate method of NG_VALIDATORS is doing that. But I need to change the css class of customComponent so background turns red. I spend severals hours on this and cannot figure it out:
// my-input.component.html
<textarea
[value]="value"
(input)="onChange($event.target.value)"
[ngClass]="{'failed-validation' : this.validationError }">
</textarea>
// my-input.component.ts
validate(control: FormControl): ValidationErrors | null {
if(!this.validationError){
return null
} else {
return { required: true };
}
}
private onChange(val) {
if(val.length > 0) {
this.value = val
this.validationError = false;
} else {
this.validationError = true;
}
// update the form
this.propagateChange(val);
}
// app.component.html
<form [formGroup]="reactiveForm">
<app-my-input formControlName="result"></app-my-input>
<input
value="Submit"
(click)="nextStep($event)"
type="button">
</form>
// app.component.ts
private nextStep(event){
// How do I dynamically change the class of the form control so I can change the style if formControl invalid when clicking the nextStep button
// pseudocode:
// if( !this.reactiveForm.controls['result'].valid ){
// this.reactiveForm.controls['result'].addClass('failed-validation');
// }
}
How can I get the css of the formControl to change in another component?
Since you using reactive form I have modified your custom form control. Here I have Use Injected NgControl Which is base class for all FormControl-based directives extend.
Try this:
import { Component, Input, forwardRef, OnInit } from "#angular/core";
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
NgControl,
NG_VALIDATORS,
FormControl,
ValidationErrors,
Validator
} from "#angular/forms";
#Component({
selector: "app-my-input",
templateUrl: "./my-input.component.html",
styleUrls: ["./my-input.component.scss"]
})
export class MyInputComponent implements ControlValueAccessor, OnInit {
private propagateChange = (_: any) => {};
value = "";
onTouch: () => void;
constructor(public controlDir: NgControl) {
controlDir.valueAccessor = this;
}
writeValue(value) {
this.value = value;
}
registerOnChange(fn) {
this.propagateChange = fn;
}
registerOnTouched(fn) {
this.onTouch = fn;
}
onChange(value) {
this.propagateChange(value);
}
ngOnInit() {
const control = this.controlDir.control;
control.setValidators([control.validator ? control.validator : null]);
control.updateValueAndValidity();
}
}
Example
For More Information Forms Check this
Requested behaviour:
I would like to create an autosuggest search box using the Algolia Angular instant search feature. This search box should have an Angular Material design. Therefore I would like to use the <ais-search-box></ais-search-box> component, but I would like to add my own template.
Current State
I recreated a working Angular Instant Search component. As requested, the component delivers the correct search results if I type in a string.
issue
Now, I would like to replace the standard <ais-search-box></ais-search-box> and replace it with my custom <app-search-box></app-search-box>. I set up my code by following the Customize the UI documentation of their search box component. If I do so, I get the following error:
Member 'refine' is not callable
Is that the reason why my component does not work anymore? If so, how can I fix it?
my not working custom search-box component
import { Component, Inject, forwardRef } from '#angular/core';
import { BaseWidget, NgAisInstantSearch } from 'angular-instantsearch';
import { connectSearchBox } from 'instantsearch.js/es/connectors';
// (keyup)="this.state.refine(input.value)" throws the error
#Component({
selector: 'app-search-box',
template: `
<input
type="text"
#input
(keyup)="this.state.refine(input.value)"
[value]="this.state.query"
/>
`
})
export class SearchBox extends BaseWidget {
public state: {
query: string;
refine: Function;
clear: Function;
isSearchStalled: boolean;
widgetParams: object;
};
constructor(
#Inject(forwardRef(() => NgAisInstantSearch))
public instantSearchParent
) {
super('SearchBox');
this.createWidget(connectSearchBox, {
// instance options
});
}
}
my working search component
import { Component } from '#angular/core';
import { environment } from 'src/environments/environment';
#Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.css']
})
export class SearchComponent {
searchConfig = {
...environment.algolia,
indexName: 'ideas'
};
showResults = false;
constructor() { }
searchChanged(query) {
if (query.length) {
this.showResults = true;
} else {
this.showResults = false;
}
}
}
my working html template
<ais-instantsearch [config]="searchConfig">
<!---working ais search box component-->
<ais-search-box (change)="searchChanged($event)"></ais-search-box>
<!---should be replaced by my custom search-box component.-->
<!--<app-search-box (change)="searchChanged($event)"></app-search-box>-->
<ais-hits *ngIf="showResults">
<ng-template let-hits="hits">
<div *ngFor="let hit of hits">
<div class="bio">
{{ hit.ideaText }}
</div>
</div>
</ng-template>
</ais-hits>
</ais-instantsearch>
check that with Angular Material
HTML
<mat-form-field class="col-sm-10" (keydown)="keyDownFunction($event)">
<input matInput type="search" placeholder="Buscar:" name="search" [(ngModel)]="searchbox"
(keyup)="onQuery($event)" (ngModelChange)="updatedVal($event)">
</mat-form-field>
TS
public searchbox;
private query: string = "";
//Enter KEY
keyDownFunction($event) {
const text = this.query;
if ($event.keyCode == 13) {
this._router.navigate([`/search/${text.replace(/\s+/g, '_')}`])
}
}
//Button KEY
BtnFunction($event) {
const text = this.query;
if ($event) {
this._router.navigate([`/search/${text.replace(/\s+/g, '_')}`])
}
}
// Algolia KEY
onQuery($event) {
this.query = $event.target.value;
}
I'd like to be able to access the SearchResults component, (when it has been clicked), in the root component (AppComponent) as I'm looking to set different properties on the SearchResults component such as;
I'd like to set an attribute on the SearchResults component so that it shows the "close" text
Also, I'd to set the click event on the SearchResults to redirect elsewhere or actually enable it as a multi-select so that it stays selected until a user proceeds to the next step for example.
I'm trying to make the SearchResults and SearchResult components as re-usable as possible so we're able to state in the parent component which would include the <app-searchresults> selector what action we'd like our SearchResults components to actually be when they are clicked.
The only way I can really see doing this is using EventEmitter to pass the event up once through the SearchResult component then onto the parent component and then a Service to hold selected values but I'm still stuck around enabling the SearchResults component as either a component which redirects when clicked or stays selected? Is this actually possible or do I need to create a different SearchResults component for each different state I'd like?!
export class AppComponent {
#ViewChildren(SearchresultComponent) components: QueryList<SearchresultComponent>;
name = 'Angular';
ngAfterViewInit() {
this.components.changes.subscribe((r) => { console.log(r) });
}
}
SearchResults.ts
#Component({
selector: 'app-searchresults',
templateUrl: './searchresults.component.html',
styleUrls: ['./searchresults.component.css']
})
export class SearchresultsComponent implements OnInit {
#ViewChildren(SearchresultComponent) components: QueryList<SearchresultComponent>;
constructor() { }
ngOnInit() {
}
}
SearchResults.html
<h1>Search Results<h1>
<app-searchresult result ="first"></app-searchresult>
<app-searchresult result ="second"></app-searchresult>
<app-searchresult result ="third"></app-searchresult>
SearchResult.ts
#Component({
selector: 'app-searchresult',
templateUrl: './searchresult.component.html',
styleUrls: ['./searchresult.component.css']
})
export class SearchresultComponent implements OnInit {
#Input()
result: string;
isSelected: boolean;
constructor() { }
ngOnInit() {
}
toggleClickedState(){
if(!this.isSelected){
this.isSelected = !this.isSelected;
}
}
}
SearchResult.html
<div>
<p (click)=toggleClickedState() [ngClass]="isSelected? 'selected' : '' "> Search Result : {{result}}</p>
<p *ngIf="isSelected" class="cross" (click)="isSelected = false;">close</p>
<div>
I've included a link to structure of an app that references the above;
https://stackblitz.com/edit/angular-cjhovx
I am using 'angular2-virtual-scroll' to implement load on demand. The items used to be driven by observable's using the async pipe triggered by the parent component. Now i am trying to call my service from the child. The call is successful and i get my data, i need to use the subscribe event to apply other logic. The issue is change detected does not appear to be working when i update my arrays in the subscribe function. I have read other similar issues but i have had no luck finding a solution.
This is the main component where the service calls are used. The inital request is done from the onInit. And then when you scroll down fetchMore is called.
import { Component, OnInit, Input, OnDestroy } from '#angular/core';
import { Store } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { User } from './../models/user';
import { Role } from './../../roles/models/role';
import { UsersService } from './../services/users.service';
import { ChangeEvent } from 'angular2-virtual-scroll';
import { promise } from 'selenium-webdriver';
import { VirtualScrollComponent } from 'angular2-virtual-scroll';
import { Subscription } from 'rxjs/Subscription';
#Component({
selector: 'app-users-list',
template: `
<div class="status">
Showing <span class="">{{indices?.start + 1}}</span>
- <span class="">{{indices?.end}}</span>
of <span class="">{{users?.length}}</span>
<span>({{scrollItems?.length}} nodes)</span>
</div>
<virtual-scroll [childHeight]="75" [items]="users" (update)="scrollItems = $event" (end)="fetchMore($event)">
<div #container>
<app-user-info *ngFor="let user of scrollItems" [roles]="roles" [user]="user">
<li>
<a [routerLink]="['/users/edit/', user.id]" class="btn btn-action btn-edit">Edit</a>
</li>
</app-user-info>
<div *ngIf="loading" class="loader">Loading...</div>
</div>
</virtual-scroll>
`
})
export class UsersListComponent implements OnInit, OnDestroy {
users: User[] = [];
#Input() roles: Role[];
currentPage: number;
scrollItems: User[];
indices: ChangeEvent;
readonly bufferSize: number = 20;
loading: boolean;
userServiceSub: Subscription;
constructor(private usersService: UsersService) {
}
ngOnInit() {
this.reset();
}
ngOnDestroy() {
if(this.userServiceSub) {
this.userServiceSub.unsubscribe();
}
}
reset() {
this.loading=true;
this.currentPage = 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = users;
});
}
fetchMore(event: ChangeEvent) {
if (event.end !== this.users.length) return;
this.loading=true;
this.currentPage += 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = this.users.concat(users);
});
}
}
From what i have read this could be a context issue but i am not sure. Any suggestions would be great.
"EDIT"
Looking at the source code for the plugin component i can see where the change event is captured.
VirtualScrollComponent.prototype.ngOnChanges = function (changes) {
this.previousStart = undefined;
this.previousEnd = undefined;
var items = changes.items || {};
if (changes.items != undefined && items.previousValue == undefined || (items.previousValue != undefined && items.previousValue.length === 0)) {
this.startupLoop = true;
}
this.refresh();
};
If i put a breakpoint in this event it fires on the initial load, so when we instantiate the array to []. It fires when i click on the page. But it does not fire when the array is update in the subscribe event. I have even put a button in that sets the array to empty, and that updates the view so the subscribe function must be breaking the change detection.
So when you say the change detection does not appear to be working, I assume you are referring to this: *ngFor="let user of scrollItems"?
I have not used that particular component nor do I have any running code to work with ... but I'd start by taking a closer look at this:
<virtual-scroll [childHeight]="75"
[items]="currentBuffer"
(update)="scrollItems = $event"
(end)="fetchMore($event)">
Maybe change the (update) to call a method just to ensure it is emitting and that you are getting what you expect back from it.
EDIT:
Here is an example subscription that updates the primary bound property showing the data for my page:
movies: IMovie[];
getMovies(): void {
this.movieService.getMovies().subscribe(
(movies: IMovie[]) => {
this.movies = movies;
this.performFilter(null);
},
(error: any) => this.errorMessage = <any>error
);
}
The change detection works fine in this case. So there is most likely something else going on causing the issue you are seeing.
Note that your template does need to bind to the property for the change detection to work. In my example, I'm binding to the movies property. In your example, you'd need to bind to the users property.
So change detection was not firing. I had to use "ChangeDetectorRef" with the function "markForCheck" to get change detection to work correctly. I am not sure why so i definitely have some research to do.