I am working on Angular 7 application using angular primeng and new to Angular. This is the scenario.
a) Fetched user data from external api but i need to display user based on the input search. I tried to read the following documentation "Multiple" select https://www.primefaces.org/primeng/#/autocomplete but not able to display value based on the search.
b) I have added Add button on the right side of search section. Upon clicking the add button, user should be displayed below it.
Please see the code snippet.
home.component.html
<div class="ui-g">
<div class="ui-g-9">
<p-autoComplete [(ngModel)]="patients" [suggestions]="filteredUsersMultiple" (completeMethod)="filterCountryMultiple($event)"
[minLength]="1" placeholder="Users" field="name" [multiple]="true">
</p-autoComplete>
<ul>
<li *ngFor="let c of patients">{{c}}</li>
</ul>
</div>
<div class="ui-g-3">
<button pButton type="button" label="Add" class="ui-button-secondary"></button>
</div>
</div>
country.service.ts
export class CountryService {
constructor(private http: HttpClient) { }
getUsers() {
return this.http.get('https://jsonplaceholder.typicode.com/users');
}
}
home.component.ts
filterCountryMultiple(event) {
this.userService.getUsers().subscribe(
(val: any[]) => {
this.filteredUsersMultiple = val.map(user => user.username);
console.log(this.filteredUsersMultiple);
}
)
}
It would be really helpful if somebody could help me to find on how to display user data based on searching and display it when clicking Add button.
As per the doc https://www.primefaces.org/primeng/#/autocomplete use completeMethod to query for the results. The event contains the query text. Then create a search function and compare both query and the filteredUsersMultiple.
filterCountryMultiple(event) {
let query = event.query;
this.userService.getUsers().subscribe(
(val: any[]) => {
this.filteredUsersMultiple = val.map(user => user.username);
this.checkUsers = this.mySearch(query, this.filteredUsersMultiple);
});
}
mySearch(query, filteredUsersMultiple: any[]):any[] {
let filtered : any[] = [];
for(let i = 0; i < filteredUsersMultiple.length; i++) {
let data = filteredUsersMultiple[i];
if(data.toLowerCase().indexOf(query.toLowerCase()) == 0) {
filtered.push(data);
}
}
Related
i'm trying to display some default selected data to my angular dropdown menu but for some reason if i push() the items from inside the subscribe() method the items does not being selected and if i try adding the items outside of this specific subscribe method it adds the items to the dropdown without any problems.
NOTE: If i log my array, it gives me all the items, including the items that i added from inside the subscribe()
Part of my component:
public students = []
public loadedSelectedStudents = []
public selectedStudents = []
// Get all selected students
this.studentService.getStaffStudents(+loadedUserId).subscribe(async selectedStudents => {
for (var i = 0; i < selectedStudents.length; i++) {
var selectedStudentId: number = selectedStudents[i].studentId
var studentModel: StaffStudentModel = new StaffStudentModel()
studentModel.userId = +loadedUserId
studentModel.studentId = selectedStudentId
var resStudent: StudentModel = await this.studentService.getStudent(selectedStudentId).toPromise()
// THIS IS NOT WORKING
this.loadedSelectedStudents.push({ student_id: studentModel.studentId, student_name: resStudent.name })
this.selectedStudents.push({ student_id: studentModel.studentId, student_name: resStudent.name})
this.changeDetectorRef.detectChanges();
}
})
// THIS IS WORKING
this.selectedStudents.push({student_id: 5, student_name: "Jonh"})
Part of my service:
getStaffStudents(userId: number) : any {
return this.http.get<StudentModel[]>(this.baseUrl + 'Api/GetStaffStudents/' + userId).pipe(map(res => res))
}
My dropdown settings and FormGroup:
this.studentsDropDownSettings = {
idField: 'student_id',
textField: 'student_name',
allowSearchFilter: true,
noDataAvailablePlaceholderText: "No Available Students"
}
this.studentsDropDownForm = this.fb.group({
studentsItems: [this.selectedStudents]
});
And my HTML element:
<div *ngIf="showStudensMenu" class="form-group">
<form [formGroup]="studentsDropDownForm">
<label for="exampleFormControlSelect1">Students *</label>
<ng-multiselect-dropdown
[settings]="studentsDropDownSettings"
[data]="students"
(onSelect)="onStudentSelected($event)"
(onSelectAll)="onAllStudentsSelected()"
(onDeSelect)="onStudentDeSelected($event)"
(onDeSelectAll)="onAllStudentsDeSelected()"
formControlName="studentsItems">
</ng-multiselect-dropdown>
</form>
</div>
But the multiselect is consuming [data]="students" shouldn't you push the new items to students[]? If the selected values are indeed read from this.selectedStudents you could trigger change detection by resetting the items using spread ... e.g this.selectedStudents = [...this.selectedStudents].
This happens because the html part of the component loads but the data
hasn't come from the api yet i.e. subscription is not yet complete.
You need to set a flag, say isLoaded as true on successful subscription and update the form as well. In html, use a ngIf statement so that the dropdown does not load without the data.
<div *ngIf="isLoaded && showStudensMenu" class="form-group">
<form [formGroup]="studentsDropDownForm">
<!-- your code -->
</form>
</div>
Subscription:
this.studentService.getStaffStudents(+loadedUserId).subscribe(async selectedStudents => {
//your code
this.isLoaded=true;
//Update the Form
this.studentsDropDownForm = this.fb.group({
studentsItems: [this.selectedStudents]
});
}
})
Don't forget to initialize isLoaded as false;
I need to add result to the list after place_changed event. I display the list below the input in which I find locations. Event works and result is pushed to array items. But the problem is that new added item don`t display immediately. It displayed after some time or when I click on form where this input is displayed.
.ts:
#ViewChild('locationInput', { static: true }) input: ElementRef;
autocomplete;
items = [];
ngOnInit() {
this.autocomplete = new google.maps.places.Autocomplete(this.input.nativeElement, this.localityOptions);
this.autocomplete.addListener('place_changed', () => {
this.addToListSelectedItem();
});
}
public addToListSelectedItem() {
if (this.input.nativeElement.value) {
this.items.push(this.input.nativeElement.value);
this.input.nativeElement.value = '';
}
}
.html:
<input
#locationInput
class="shadow-none form-control"
formControlName="locality"
placeholder=""
[attr.disabled]="locationForm.controls['region'].dirty ? null : true"
/>
<div *ngFor="let item of items; let index = index">
<div class="listOfLocation">
<div class="itemList">{{ item }}</div>
<img [src]="icons.cross" class="delete-button-img" alt="edit-icon" (click)="deleteTask(index)" />
</div>
</div>
Thanks for the help!
Probably your component Change Detection Strategy is OnPush or google autocomplete is running outside zone.js:
changeDetection: ChangeDetectionStrategy.OnPush
And since items is array and it is stored in memory by reference you need to run manually change detenction:
constructor(private cdr: ChangeDetectorRef)
public addToListSelectedItem() {
...
this.input.nativeElement.value = '';
this.cdr.detectChanges();
Even better would be to work with an RxJS Subject, an Observable for items$ and use the async pipe. The async pipe works like magic what concerns updating the template :-)!
#ViewChild('locationInput', { static: true }) input: ElementRef;
autocomplete;
itemsSubject$ = new Subject<any[]>();
items$ = this.itemsSubject$.asObservable();
// Use a separate array to hold the items locally:
existing = [];
ngOnInit() {
this.autocomplete = new google.maps.places.Autocomplete(this.input.nativeElement, this.localityOptions);
this.autocomplete.addListener('place_changed', () => {
this.addToListSelectedItem();
});
}
public addToListSelectedItem() {
if (this.input.nativeElement.value) {
// Use spread syntax to create a new array with the input value pushed at the end:
this.existing = [...this.existing, this.input.nativeElement.value];
// Send the newly created array to the Subject (this will update the items$ Observable since it is derived from this Subject):
this.itemsSubject$.next(this.existing);
this.input.nativeElement.value = '';
}
}
// I added the deleteTask implementation to show you how this works with the subject:
deleteTask(index: number) {
// The Array "filter" function creates a new array; here it filters out the index that is equally to the given one:
this.existing = this.existing.filter((x, i) => i !== index);
this.itemsSubject$.next(this.existing);
}
And in the template:
<input
#locationInput
class="shadow-none form-control"
formControlName="locality"
placeholder=""
[attr.disabled]="locationForm.controls['region'].dirty ? null : true"
/>
<!-- Only difference here is adding the async pipe and using the items$ Observable instead -->
<div *ngFor="let item of items$ | async; let index = index">
<div class="listOfLocation">
<div class="itemList">{{ item }}</div>
<img [src]="icons.cross" class="delete-button-img" alt="edit-icon" (click)="deleteTask(index)" />
</div>
</div>
For a working example of the RxJS Subject in this concept, see https://stackblitz.com/edit/angular-ivy-dxfsoq?file=src%2Fapp%2Fapp.component.ts.
Maybe beyond this question, but since you're using a reactive form, why not use this.locationForm.get('locality').setValue('...') to set the input instead of using a ViewChild for working with the input? More control this way then using the ViewChild.
hope you're okey.
I have a blog component which obtains posts from a DB via http request.
So, what i'm trying is, when the posts are in my component i want to filter them with a function.
I'm calling this function after get the posts in the subscribe method, but it doesn't works.
Function to filter the posts:
filtrarArticles(tipus)
{
var articles = document.getElementsByClassName("article") as HTMLCollectionOf<HTMLElement>;
for (var i = 0; i < articles.length; i++)
{
var tipusArticle = this.articles[i].categoria;
if (tipus == tipusArticle)
{
articles[i].style.display = "";
}
else
{
articles[i].style.display = "none";
}
}
}
HTML code that filters my function:
<div class="articles row w-100 align-self-end d-flex justify-content-center">
<div class="article col-xl-3 mb-md-0 mb-3" *ngFor="let article of articles; index as i" (click)="obrirModalArticle(article)">
<img class="imatgeAllargada d-block w-100" src="https://drive.google.com/uc?id={{article.img}}">
<br>
<h4>{{article.title_article}}</h4>
<p [innerHTML]="article.descripcio1"></p>
</div>
</div>
The articles array have one property which name is "categoria" an it's the key to filter it.
I call the filter function here:
this.articleService.obtenirArticles(this.idioma).subscribe(
res=>
{
this.articles = res;
this.filtrarArticles('generals');
},
error =>
{
alert("No s'han pogut obtenir els articles, intenta-ho més tard.");
});
this.translate.onLangChange.subscribe((event: LangChangeEvent) =>
{
this.idioma = event.lang;
this.articleService.obtenirArticles(this.idioma).subscribe(
res=>
{
this.articles = res;
},
error =>
{
alert("No s'han pogut obtenir els articles, intenta-ho més tard.");
}
)
});
This code is on init method. And I don't know why doesn't works?
It is considered a bad practice to access the DOM directly, one of the reasons being that you should not manipulate the template directly but rather let Angular do it for you. So what you should be doing instead is, create a model and let Angular know about it, bind it to the template and once it changes Angular will update the template for you.
Component
export class YourComponent {
allArticles; // Property to hold the result of the response
filteredArticles; // Property to hold what is going to be rendered on the template
...
ngOnInit() {
this.articleService.obtenirArticles(this.idioma).subscribe(res => {
this.allArticles = res; // Save the original response
this.filteredArticles = this.filtrarArticles('generals'); // Get the articles filtered and show this property on the template
},
error => {
alert("No s'han pogut obtenir els articles, intenta-ho més tard.");
});
this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
this.idioma = event.lang;
this.articleService.obtenirArticles(this.idioma).subscribe(res => {
this.allArticles = res; // Save the original response
this.filteredArticles = [...this.allArticles]; // Clone the response array unfiltered
},
error => {
alert("No s'han pogut obtenir els articles, intenta-ho més tard.");
})
});
}
}
Filter function
filtrarArticles(tipus) {
// The filter method will return a copy of the array once it has been iterated over each item.
// If the condition is met, the item is going to be included in the resulting array, otherwise it won't.
return this.allArticles.filter(article => article.categoria === tipus);
}
Then in your template, you iterate over the filterArticles array, once that array change, Angular will update the template.
Template
<div class="articles row w-100 align-self-end d-flex justify-content-center">
<div class="article col-xl-3 mb-md-0 mb-3" *ngFor="let article of filteredArticles; index as i" (click)="obrirModalArticle(article)">
<img class="imatgeAllargada d-block w-100" src="https://drive.google.com/uc?id={{article.img}}">
<br>
<h4>{{article.title_article}}</h4>
<p [innerHTML]="article.descripcio1"></p>
</div>
When user types in an input box and hit search button, I want to filter the data on the UI based i.e if the username starts with the entered text. I don't want to call the API again and again.
Using JavaScript Fetch API concept, I've tried to search by username but it is calling API on every search I made
This is what I've done
function searchData(){
let url = 'https://jsonplaceholder.typicode.com/users';
let data = document.getElementById("usersearch").value;
//passing the username, user enters as a url to the showData function
url = url+"?username="+`${data}`;
showData(url);
}
function showData(url){
fetch(url)
.then(response => response.json())
.then(data => {
let out = '<h2 class = "mt-3 mb-3">Search Result</h2>';
data.forEach(user =>{
out += `
<ul class = "mylist card">
<li id = "myli" class = "card-body text-primary pl-3"> ${user.name} </li>
<li class = "card-body text-secondary"> ${user.email} </li>
<li id = "myli2" class = "card-body text-info"> ${user.website} </li>
</ul>
`;
})
//Edit
document.getElementById("output").innerHTML = out;
})
.catch(err => console.log('Error : ',err.message))
}
//Edit
<button class = "btn btn-secondary" id = "btn1" onclick = "searchData()">Search</button>
I don't want to call the API again and again or every click of search button
Edits :
I am calling searchData() method using onclick function
Client-side filtering is never a good idea. As a general rule of thumb, you should prefer server-side filtering. Hitting the API guarantees correct results while client-side filtering may not. I have written a similar answer to this one.
Read here: which way is better?
You can declare two variables, one to store the entire user list and one to filter by your input and display.
Plus, it is more clear for you and the other devs when you seperate the fetch and the UI. So you must refactor showData() to seperate theses process.
So here the process =
Fetch all users and store them into a variable that will never change
To display them you use a second variable that will filter your result by your input
Each time you search , simply execute the searchData() function
Here is a functional snippet based on what i explained
let users = []; // user list
let usersFiltered = []; //user you display
// you wanna fetch ALL user without filter and store in your users variable
function fetchUsers() {
let url = 'https://jsonplaceholder.typicode.com/users';
fetch(url)
.then(response => response.json())
.then(data => {
users = data;
usersFiltered = users;
showData();
}).catch(err => console.log('Error : ', err.message));
}
// then this function only shows filtered users
function showData() {
let out = '<h2 class = "mt-3 mb-3">Search Result</h2>';
usersFiltered.forEach(user => {
out += `
<ul class = "mylist card">
<li id = "myli" class = "card-body text-primary pl-3"> ${user.name} </li>
<li class = "card-body text-secondary"> ${user.email} </li>
<li id = "myli2" class = "card-body text-info"> ${user.website} </li>
</ul>
`;
});
document.getElementById('users').innerHTML = out;
}
// the search data will filter your inputs
function searchData() {
let data = document.getElementById("usersearch").value;
usersFiltered = users.filter(user => {
// here i filter by name but you can implement what filter you want
return user.name.includes(data);
});
showData();
}
fetchUsers(); // to fetch users at the begening
<input id="usersearch" type='text' />
<button class = "btn btn-secondary" id = "btn1" onclick = "searchData()">Search</button>
<div id="users"></div>
I am trying to build a picture library in a responsive grid with data coming down from a Mongo DB. Data from db is in this form. name, image, keyword, bio, reference", searchable,
I start with an ng-repeat to display category selection checkboxes based on the picture keywords. This seems to work fine.
p CATEGORIES:
span.formgroup(data-ng-repeat='category in mongoController.catData')
input.checks( type='checkbox', ng-model='mongoController.filter[category]')
| {{category}}
Here is the factory that sorts and identifies the keyword checkboxes:
getCategories: function (cat) {
return (cat || []).map(function (pic) {
return pic.keyword;
}).filter(function (pic, idx, array) {
return array.indexOf(pic) === idx;
});
}
From there I filter an ng-repeat to display pictures based on the checkbox selection, and/or a search field which works well also.
input.form-control.input-med(placeholder="Search" type='text', ng-model="search.searchable")
br
div.light.imageItem(data-ng-repeat="picture in filtered=( mongoController.allData | filter:search | filter:mongoController.filterByCategory)")
a(data-toggle="modal", data-target="#myModal", data-backdrop="static", ng-click='mongoController.selectPicture(picture)')
img( ng-src='../images/{{picture.image}}_tn.jpg', alt='Photo of {{picture.image}}')
p Number of results: {{filtered.length}}
Functions to display picture lists:
//Returns pictures of checkboxes || calls the noFilter function to show all
mongoController.filterByCategory = function (picture) {
return mongoController.filter[picture.keyword] || noFilter(mongoController.filter);
};
function noFilter(filterObj) {
for (var key in filterObj) {
if (filterObj[key]) {
return false;
}
}
return true;
}
If you click one of the pictures a modal box pops up with all of the input fields where you can edit image specific fields.
The part I am really struggling with is how to gather the usable data from the filtered ng-repeat to use in the controller so when the modal box is up I can scroll right or left through the other pictures that met the criteria of the search.
Any suggestions would help, even why the hell are you doing it this way?
When you declare the ng-repeat that filters the pictures, your filtered variable belongs to the current $scope (which I cannot infer from the question, as it stands). You could associate the filtered variable to a specific controller by using Controller As syntax: (i.e. using <elem ng-repeat="picture in ctrl.filtered = (ctrl.data | filter1 | filterN)"/>)
Example: (also in jsfiddle)
var mod = angular.module('myapp', []);
mod.controller('ctrl', function() {
var vm = this;
vm.data = ['alice', 'bob', 'carol', 'david', 'ema'];
vm.onbutton = function() {
// access here the filtered data that mets the criteria of the search.
console.log(vm.filtered);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myapp" ng-controller="ctrl as vm">
<input ng-model="search" type="text" />
<p data-ng-repeat="item in vm.filtered = (vm.data | filter:search)">
{{item}}
</p>
<p>Items: {{vm.filtered.length}}</p>
<button ng-click="vm.onbutton()">Show me in console</button>
</div>