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;
}
Related
I'm very new to Angular, and I'm really struggling to find a concise answer to this problem. I have a Form Component Here:
(I'm excluding the directives and imports as they're not really relevant)
export class JournalFormComponent implements OnInit {
public entries: EntriesService;
constructor(entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
}
The EntriesService service just stores an array of entries:
export class Entry {
constructor (
public num: number,
public name: string,
public description: string,
public text: string
) { }
}
The Form Component template renders a <h2> and a <app-input> Component for each entry in the EntriesService, which works. That looks like this:
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}"></app-input>
</div>
Here's the <app-input> Input Component:
#Component({
selector: 'app-input',
template: `
<textarea #box
(keyup.enter)="update(box.value)"
(blur)="update(box.value)">
</textarea>
`
})
export class InputComponent {
private value = '';
update(value: string) {
this.value = value;
}
getValue () {
return this.value;
}
}
The InputComponent stores the user's text perfectly, but I don't know how to pass that data to the Form Component's EntriesService to update the Entry in order to Export it or Save it later. How is this done?
I think I'm phrasing this question well, but I'm not sure. If you need clarification I'll provide it.
Not sure if it matters, but I'm using Angular 9.1.11
There are many ways to update the data from one component to another.
component to component using service or subjects
parent~child component data exchange using Input() and Output() decorators. Or by using #ViweChild() interactions.
and many more
But please do check the angular docs https://angular.io/guide/component-interaction .
Use the below simple code, u might need to include modules like FormsModule. and import Input(), Output etc
#Component({
selector: 'app-journal-form',
template: `
<div *ngFor="let entry of entries.entries; let i=index">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" [entry]="entry" [arrayIndex]="i" (updateEntry)="updateEntry($event)" ></app-input>
</div>`
})
export class JournalFormComponent implements OnInit {
constructor(private entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
updateEntry(event){
console.log(event);
this.entries[event.arrayIndex] = event.entry;
}
}
#Component({
selector: 'app-input',
template: `
<textarea [(ngModel)]="name"
(keyup.enter)="update()"
(blur)="update()">
</textarea>
`
})
export class InputComponent {
#Input() entry: any;
#Input() arrayIndex: number;
#Output() updateEntry: EventEmitter<any> = new EventEmitter();
name:string;
constructor() {
console.log(entry);
this.name = entry.name;
}
update(){
this.entry.name = this.name;
this.updateEntry.emit({entry: this.entry, arrayIndex});
}
}
Output event will help in this situation.
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" (entryChange) = "entry.text = $event"></app-input>
</div>
app-input component
export class InputComponent {
private value = '';
#Output() entryChange = new EventEmitter<string>();
update(value: string) {
this.value = value;
this.entryChange.emit(value);
}
}
Instead of entry.text = $event you can also pass it to any save function, like saveEntry($event);
I'm doing angular project and I want to let the user search/filter from existing tags (mat-chips). I have a searchbox and I can filter a normal list but when I tried to do that for tags, I'm not sure how.
My mat chips inside home.component.html.
<mc-tags [chips] = "tags" ></mc-tags>
My search box inside home.component.html
<input matInput (input)="updateQuery($event.target.value)" class="input" >
the data inside list.ts
export const tags = ['Google', 'Manufacturer'];
home.component.ts file
import { Component, OnInit } from '#angular/core';
import { users, tags } from './users.data';
#Component({
selector: 'mc-explore',
templateUrl: './explore.component.html',
styleUrls: ['./explore.component.scss']
})
export class ExploreComponent{
query: string;
users = users;
tags = tags;
updateQuery(query: string) {
this.query = query;
}
}
This is how it look like right now
Picture
this is how I usually filter normal list/data
<div [hidden]="!query">
<div *ngFor="let name of users | search:query">{{ name }}</div>
</div>
Stackblitz file without mc-tags since it's using from different components
https://stackblitz.com/edit/angular-vcklft
You can do what is described below.
Change this:
<input matInput (input)="updateQuery($event.target.value)" class="input" >
to this:
<input [formControl]="_inputCtrl" matInput class="input" >
And change this:
<mc-tags [chips] = "tags" ></mc-tags>
to this:
<mc-tags [chips]="_filteredTags" ></mc-tags>
Add this code to your component typescript:
_filteredTags = [];
_inputCtrl: FormControl = new FormControl();
private _destroy$ = new Subject<void>();
ngOnInit(): void {
this._filterTags();
this._inputCtrl.valueChanges
.pipe(takeUntil(this._destroy$))
.subscribe((value: string) => {
this._filterTags(value);
this.updateQuery(value);
});
}
ngOnDestroy(): void {
if (this._destroy$ && !this._destroy$.closed) {
this._destroy$.next();
this._destroy$.complete();
}
}
updateQuery(query: string) {
this.query = query;
}
private _filterTags(filterValue?: string) {
if (!filterValue) {
this._filteredTags = [...tags];
return;
}
this._filteredTags = this.tags.filter((v: string) =>
v.toLowerCase().includes(filterValue.toLowerCase().trim())
);
}
[UPDATE]: I've put together this stackblitz demo
How to update component when route changes. I have this component :
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { ListService } from '../list/list.service';
#Component({
selector: 'view',
template: `
<div *ngIf="!entity">
<p>Select <b (click)="showRow()">row {{entity}}</b>!</p>
</div>
<div *ngIf="entity">
<p >{{entity.id}}</p>
<p >{{entity.name}}</p>
<p >{{entity.weight}}</p>
<p >{{entity.symbol}}</p>
</div>
`,
styles: []
})
export class ViewComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private service: ListService
) {
this.route.params.subscribe(params => {
const id = parseInt(params['id']);
if (id) {
const entity = this.service.getRow(id);
this.entity = entity
}
});
}
entity;
showRow() {
console.log(this.entity);
}
ngOnInit() {
}
}
in this.entity inside constructor i have desired object but when i execute showRow this.entity is undefined, what i'm doing wrong ? I have tried to change property to different name, and it didn't work as expected, if any one knows how to resolve this or point me to right direction.
EDIT:
getRow from service
getRow(id) {
console.log(id, 'test');
return this.datasource.find(row => row.id === id);//returns good row
}
Move your code to ngOnInit() method and check will you get value or not.
ngOnInit() {
this.route.params.subscribe(params => {
const id = parseInt(params['id']);
if (id) {
const entity = this.service.getRow(id);
this.entity = entity
}
});
}
I believe you need to define/initialize the entity that is positioned above showRow, such as:
const entity = Entity;
or something along those lines. apologies I am quite new to angular as well.
I found answer to my problem, i just needed to put router-outlet in template just like that :
....
....
template: `
<router-outlet>
<div *ngIf="row?.id; else elseBlock">
<div>
<p>{{row.id}}</p>
<p>{{row.name}}</p>
<p>{{row.weight}}</p>
<p>{{row.symbol}}</p>
</div>
</div>
</router-outlet>
`,
I have a component that pulls in a value posts like so:
import { Component, OnInit} from "#angular/core";
import template from "./event.component.html";
import style from "./event.component.scss";
#Component({
selector: "EventComponent",
template,
styles: [ style ]
})
export class EventComponent implements OnInit {
posts = [];
constructor() {}
ngOnInit() {
this.posts = {'test': 0,'test': 1};
}
}
This is then looped over in a html template like so AND injected into another component in this case called "mapCompenent" it is also filter in the html using a pipe:
loop 'EventComponent' content
<input id="search_events" type="text" name="search_events" [(ngModel)]="search" ngDefaultControl/>
<mapCompenent [(posts)]="posts"></mapCompenent>
<div class="col s6 m6 l4 cards-container" *ngFor="let post of posts | searchPipe:'name':search "></div>
filter
import { Pipe, PipeTransform, Input, ChangeDetectorRef } from '#angular/core';
import { FormGroup, FormControl, FormBuilder, Validators } from '#angular/forms';
#Pipe({
name : 'searchPipe',
pure: false,
})
export class SearchPipe implements PipeTransform {
public transform(value, key: string, term: string) {
if(term === '' || typeof term === undefined ){
return value;
}
return value.filter((item) => {
if (item.hasOwnProperty(key)) {
if (term) {
let regExp = new RegExp('\\b' + term, 'gi');
//this.ref.markForCheck();
return regExp.test(item[key]);
} else {
return true;
}
} else {
return false;
}
});
}
}
mapComponent
import { Component, OnInit, Input, OnChanges, SimpleChanges, SimpleChange } from "#angular/core";
import template from "./map.component.html";
import style from "./map.component.scss";
#Component({
selector: 'mapCompenent',
styles: [ style ],
template
})
export class MapComponent implements OnInit, OnChanges{
#Input() posts: object = {};
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
const posts: SimpleChange = changes.posts;
console.log('prev value: ', posts.previousValue);
console.log('got posts: ', posts.currentValue);
}
}
As soon as the page is loaded the mapcomponent grabs the ngOnChanges BUT not when the filter is used to filter the posts, the loop updates the posts fine and the filter works there the problem is the mapcomponent. What is the best way to notify the mapcomponent of a change to the posts Object?
The pipe will not overwrite the original posts property in EventComponent, so you are only using the filtered version in the *ngFor:
<input id="search_events" type="text" name="search_events" [(ngModel)]="search" ngDefaultControl/>
<mapCompenent [(posts)]="posts"></mapCompenent>
<div class="col s6 m6 l4 cards-container" *ngFor="let post of posts | searchPipe:'name':search "></div>
One solution is to add the pipe to the <mapComponent>'s posts attribute as well, but note it can't be two-way binded ([()]) then, you should change it to one-way ([]).
<input id="search_events" type="text" name="search_events" [(ngModel)]="search" ngDefaultControl/>
<mapCompenent [posts]="posts | searchPipe:'name':search"></mapCompenent>
<div class="col s6 m6 l4 cards-container" *ngFor="let post of posts | searchPipe:'name':search"></div>
A better solution would be to inject that pipe into the EventComponent constructor, listen for changes on the search input or watching search and update another attribute, let's say filteredPosts accordingly using the pipe, and use that one both in the *ngFor and the <mapCompenent>:
#Component({ ... })
export class EventComponent implements OnInit {
posts = [];
filteredPosts = [];
constructor(private searchPipe: SearchPipe) {}
ngOnInit() {
this.posts = ...;
this.form.search.valueChanges.subscribe((value) => {
this.filteredPosts = this.searchPipe.transform(this.posts, 'name', value);
});
}
}
On my parent page I have a link here:
<a (click)="showPermissionsRates(5757);">Link</a>
The function sets it:
showPermissionsRates(item) {
this.currentEventPoolId = item;
}
With a child component on the parent page here:
<app-event-pools-permissions-rates [eventPoolId]="currentEventPoolId "></app-event-pools-permissions-rates>
And then in my child component TS file I use:
inputs: ['eventPoolId']
But how do I get that value of '5757' in the child component? Such as using alert?
You should be able to just use #Input() on the child property.
I've put this together showing a VERY basic example, but without more to go on regarding your issues, it's hard to know what you need:
https://plnkr.co/edit/y9clOla1WrPFmhMJoz7o?p=preview
The gist is to use #Input() to mark your inputs in the child component, and map those in the template of the parent.
import {Component} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
import { ChildComponent } from 'child.component.ts';
#Component({
selector: 'my-app',
template: `
<div>
<button (click)="changeProperty('ABC 123')">Click Me!</button>
<child-component [childProperty]="parentProperty"></child-component>
</div>
`,
})
export class App {
public parentProperty: string = "parentProp";
public changeProperty(newProperty: string) : void {
this.parentProperty = newProperty;
}
}
Then, in the child:
import {Component, Input} from '#angular/core'
#Component({
selector: 'child-component',
template: `
<div>Hello World: {{ childProperty }}</div>
`,
})
export class ChildComponent {
#Input()
childProperty:string;
constructor() {
this.childProperty = 'childProp'
}
}
I think you are setting value to at input variable in a click event, then you have to listen for it in the child component constructor using ngonchanges
ngOnChanges(changes: SimpleChanges) {
if(changes['eventpoolid'] && changes['eventpoolid'].currentValue) {
// you get updated value here
}
}