I'm having a problem creating a search filter, I use the * ngFor in the module and the name usually works like this:
(game.name.toLowerCase().includes(typed))
But the platforms that are coming in array only work when I put the index:
(game.platform[i].toLowerCase().includes(typed))
However this platform is dynamic and I can not use a for or something of the genre inside the filter
Json
[
{
"name":"GTA",
"platform":[
"xbox",
"playstation"
]
}
]
Component
<tr *ngFor="let game of (games | searchGame:SearchedText.value)">
<td>{{game.name}}</td>
<td>{{game.category}}</td>
<td>{{game.platform | separator}}</td>
// "platform":["Playstation 4","Xbox One"]
<td>{{game.price | currency}}</td>
<td>{{game.quantity}}</td>
<td>{{game.production ? 'Sim' : 'Não'}}</td>
<td>{{game.description}}</td>
<td><i class="fa fa-pencil icon" aria-hidden="true" (click)="staticModal.show()"></i></td>
<td><i class="fa fa-times icon" aria-hidden="true" (click)="staticModal.show()"></i></td>
</tr>
Pipe
transform(game, typed){
typed = typed.toLowerCase();
return game.filter( game =>
(game.name.toLowerCase().includes(typed)) ||
(game.category.toLowerCase().includes(typed)) ||
(game.platform.toLowerCase().includes(typed)) || // Error in this line
(game.price.toLowerCase().includes(typed)) ||
(game.quantity.toLowerCase().includes(typed)) ||
(game.production.toLowerCase().includes(typed)) ||
(game.description.toLowerCase().includes(typed))
);
}
If I understand your question correctly, your filter is not doing what you want.
The first pipe parameter should be an array. I think just 'any' works but 'any[]' is more clean :-)
You are doing:
tranform(game: any,...)
Try this:
transform(game: any[], typed: string){
....
return game.filter(game => game.name.toLowerCase().indexOf(typed) > -1 ||
....
Further in the beginning your search token (typed) might be null or empty.
For that you should add the following to return the array unfiltered. Otherwise you will not see anything in your *ngFor.
if (typed == null || typed == "")
return game;
Hope this helps a little.
Edit: Replace the erroring line with
game.filter(game => game.platforms.some(el => el.toLocaleLowerCase().includes(typed)))
Related
I have an array that is populated after a .subscribe to my API. Console shows it populated as expected. Accessing an element of the array results to an error thrown because of it being undefined
<div *ngIf="!invoices || invoices.length === 0">
No invoices
</div>
<div *ngIf="invoices || async ">
{{ invoices[0]?.invoice_id || async}}
</div>
If I remove the elvis operator my content will load fine however the console will throw errors InvoicesComponent.html:10 ERROR TypeError: Cannot read property 'invoice_id' of undefined until the array gets populated from the subscribe function.
The invoices array is initialised in my service
invoices: Array<Invoice> = [];
And I populate the array
getInvoices(){
var _invoices = this.invoices;
if(this.afAuth.user){
// users/uid/invoices/invoice_id/
var userRef = this.afs.doc(`users/${this.afAuth.auth.currentUser.uid}`)
userRef.collection('invoices').get().subscribe(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
_invoices.push({
'invoice_id': doc.id,
'customer_company': doc.data().customer_company,
'year_id':doc.data().year_id,
'date_created': doc.data().date_created,
'date_modified': doc.data().date_modified})
});
console.log(_invoices)
});
return _invoices
}
Based on the suggestion of trichetriche, an `Invoice class was created
import { QueryDocumentSnapshot } from "#angular/fire/firestore";
import { of } from 'rxjs'
export class Invoice {
invoice_id: string;
customer_company: string;
date_created: string;
date_modified: string;
year_id: string;
constructor(invoiceDoc: QueryDocumentSnapshot<any>){
this.invoice_id = invoiceDoc.id
this.customer_company = invoiceDoc.data().customer_company
this.date_created = invoiceDoc.data().date_created
this.date_modified = invoiceDoc.data().date_modified
this.year_id = invoiceDoc.data().year_id
}
toObservable(){
return of(this)
}
}
Original
<div *ngIf="!invoices || invoices.length === 0">
No invoices
</div>
<div *ngIf="invoices || async ">
{{ invoices[0]?.invoice_id || async}}
</div>
Edited
<ng-container *ngIf="invoices | async as invoicesSync; else noInvoices">
<p>{{ invoicesSync[0]?.invoice_id || 'No ID for invoice' }}</p>
</ng-container>
<ng-template #noInvoices>
<p>No invoices</p>
</ng-template>
1 - It's | async, not || async : | is a pipe, || is a fallback to a falsy statement.
2 - There should be a single async in your code, which create a template variable through as XXX.
3 - You don't need several conditions. Use a single one with a then statement.
i think you are using the Async pipe in wrong way .
you can passe Observable directly to template and the code will like this :
<div *ngIf="invoices|async as invoicesList; else noInvoices">
{{ invoicesList[0]?.invoice_id}}
</div>
<ng-template #noInvoices>
<div >
No invoices
</div>
</ng-template>
Right so after some research it seems that I was better off subscribing to an observable and dealing with the data as it arrives from my API with the async pipe.
So my final functions look kind of like this:
ngOnInit() {
this.observableInvoices = this.auth.getObservableInvoices().pipe(map(
(data) => data));
console.log(this.observableInvoices)
}
<li *ngFor="let invoice of observableInvoices | async; index as i">
getObservableInvoices(): Observable<any> {
this.observable_invoices = this.afs
.collection(`users/${this.afAuth.auth.currentUser.uid}/invoices`)
.valueChanges() as Observable<any[]>;
return this.observable_invoices;
}
This is a JSX gist I'm displaying on the page.
rows.push(
<tr key={i}>
<td>{this.state['Person Name'] && this.state['Person Name'][i]}</td>
<td>{this.state['Amount'] && this.state['Amount'][i]}</td>
{this.state['Memo'] && this.state['Memo'].length > 0 ? <td>{this.state['Memo'][i]}</td> : undefined}
{this.state['Comment'] && this.state['Comment'].length > 0 ? <td>{this.state['Comment'][i]}</td> : undefined}
{this.state['Incurred Date'] && this.state['Incurred Date'].length > 0 ? <td>{this.state['Incurred Date'][i]}</td> : undefined}
{this.state['Entry Date'] && this.state['Entry Date'].length > 0 ? <td>{this.state['Entry Date'][i]}</td> : undefined}
<td>{this.state['Billable'] && this.state['Billable'][i]}</td>
<td>{this.state.fileName === 'expenses.csv' ? 'Expense' : 'Time'}</td>
</tr>
)
Somehow the conditions that are falsy still display empty <td>s to the table. What did I miss?
Empty columns are shown above.
You don't need to use ternary operators at all. Simply just chain &&'s.
{this.state.Memo && this.state.Memo[i] && <td>{this.state.Memo[i]}</td>}
{this.state.Comment && this.state.Comment[i] && <td>{this.state.Comment[i]}</td>}
{this.state['Incurred Date'] && this.state['Incurred Date'][i] && <td>{this.state['Incurred Date'][i]}</td>}
{this.state['Entry Date'] && this.state['Entry Date'][i] && <td>{this.state['Entry Date'][i]}</td>}
In addition, your array seems to be badly formatted:
// 2 separate variables for the same data?
this.memo = ['a', 'b'];
this.comment = ['comment', 'comment2'];
// Why not make it an array like this?
this.rows = [
{
Memo: 'a',
Comment: 'comment'
},
{
Memo: 'b',
Comment: 'comment2'
}
];
Then you can simply do:
this.rows.map(row => (
<tr key={row.id}>
<td>{row['Person Name']}</td>
<td>{row['Amount']}</td>
{this.state.hasMemos && <td>{row.Memo}</td>}
...
</tr>
)}
A <td> shouldn't be conditional on a row level, it should be conditional on a table level. You can't simply skip TD's if there isn't data for that row, as it will throw off the whole row by shifting columns over. You should either display N/A, an empty <td></td> for rows that may not have data, or hide them entirely on a table level via something like this.state.hasMemos, if there are any memos.
If you're using the new array structure I have listed, you can use this function to determine if any row has a memo:
this.array.some(row => row.Memo);
This will return true if any row has a Memo, thus either hiding the <td> for the entire table, or displaying it for every row.
I have a list Entity called Awards which has a Name (string) and YearGiven (Entity) as its fields.
I want to show all awards grouped by Year.
ie
2017
---Bob
---Sue
2016
---Fred
2015
etc
Here is my template:
#using ToSic.SexyContent
#functions
{
// variable which will contain the sorted categories
IEnumerable<dynamic> sortedCategories;
// Prepare the data - get all categories through the pipeline
public override void CustomizeData()
{
// get all categories of these questions, then get the distinct entities
// this could all be done on 1 line, but it would be harder for people who don't know LINQ yet
var awardsInThisModule = AsDynamic(App.Data["Awards"].List);
var categoriesUsed = awardsInThisModule.SelectMany(q => ((List<DynamicEntity>)q.YearGiven));
var distinctCategories = categoriesUsed.Select(AsEntity).Distinct(); // Distinct only works reliably when cast as entity
sortedCategories = AsDynamic(distinctCategories).OrderBy(q => q.Year);
}
}
<link rel="stylesheet" href="#App.Path/assets/awards.css" data-enableoptimizations="true" />
#foreach (var cat in sortedCategories)
{
<h3> #cat.Year</h3>
foreach (var q in AsDynamic(App.Data["Awards"].List).Where(t => t.Name == "Bob").OrderBy(q => q.Name))
{
//this works fine and puts Bob against each year
<h2>#q.Name</h2>
}
foreach (var q in AsDynamic(App.Data["Awards"].List).Where(t => t.Year.Select(a => AsDynamic(a).Year) == "2017"))
{
//this is what I actually want to do and fails
<h2>#q.Name</h2>
}
<br />
}
I started by changing the Where clause to t.YearGiven == 2016 but that gives an error "Operator '==' cannot be applied to operands of type 'System.Collections.Generic.List' and 'int' a" - I assume because YearGiven is an Entity and so is actually a List<>.
So then I changed to the next foreach in the code and got this error:-
"Cannot use a lambda expression as an argument to a dynamically dispatched operation without first casting it to a delegate or expression tree type."
I can't find any template example that does what I'm trying to do and nothing I do works.
N.B. I've hardcoded '2017' in there for now to keep things simple but it will obviously be doing each Year found in the outer loop.
Here is a simple example with a similar schema if you want to adapt it. I am basically using a variable (currCat) to keep track and handle the 'on-change of category'. Hopefully you can ignore all the expando/collapse stuff. Here is what the final looks like:
http://www.blackandco.com/Vendor-Linecard
<div id="vendor-list" role="tablist" class="small">
#{
int currCat = 0;
int firstCo = 851; // Abrasives
foreach (var aCat in AsDynamic(App.Data["CompanyCategories"])
.Where(c => c.CategoryActiveYN == true)
.OrderBy(c => c.CategoryName)
)
{
currCat = aCat.EntityId;
<div class="card">
<div class="card-header" role="tab" id="#string.Format("{0}{1}", "heading", #currCat)">
<h5 class="mb-0#((currCat == firstCo) ? "" : " collapsed")" data-toggle="collapse" href="#string.Format("{0}{1}", "#collapse", #currCat)"
aria-expanded="#((currCat == firstCo) ? "true" : "false")" aria-controls="#string.Format("{0}{1}", "collapse", #currCat)">
#aCat.CategoryName
</h5>
</div>
<div id="#string.Format("{0}{1}", "collapse", #currCat)" class="collapse#((currCat==firstCo) ? " show" : "")" role="tabpanel" aria-labelledby="#string.Format("{0}{1}", "heading", #currCat)" data-parent="#accordion" aria-expanded="#((currCat==firstCo) ? "true" : "false")">
<div class="card-body">
<ul>
#foreach (var vComp in AsDynamic(App.Data["Company"])
.Where(v => v.CompanyActiveYN && v.IncludeOnVendorCards)
.OrderBy(v => v.CompanyName)
)
{
foreach (var vCat in vComp.CompanyCategory)
{
if (vCat.EntityId == currCat)
{
<li>#vComp.CompanyName<span></li>
}
}
}
</ul>
</div>
</div>
</div>
}
}
</div>
I'm still stuck with a OrderBy issue. Data comes from $http, and looks like this:
[
{
"number":1,
"timetable": [
{
"name":"Station1",
"time":"2016-05-18T18:14:00.000Z"
},
{
"name":"Station2",
"time":"2016-05-18T18:18:00.000Z"
}
]
},
{
"number":2,
"timetable": [
{
"name":"Station1",
"time":"2016-05-18T18:24:00.000Z"
},
{
"name":"Station2",
"time":"2016-05-18T18:28:00.000Z"
}
]
}
]
So what I need is to view rows where name is for example Station2, and I need them in order by time. The rows aren't ordered by time, nor by number. Amount of timetable rows varies, so row numbers don't help either. Is it possible to order them inside ng-repeat, in style of "OrderBy time where name='Station2' "?
EDIT:
At the moment I'm showing the results without any ordering, only with filtering. Current PHP:
<tr ng-repeat="x in rows | limitTo:5">
<td>
<a href="aikataulu.php?n={{x.number}}">
<span class="label label-primary line-{{x.lineID}}">{{x.lineID}}</span> {{x.number}}
</a>
</td>
<td>
<span ng-repeat="y in x.timetable | limitTo:-1">{{y.name}}</span> //This is for showing the destination
</td>
<td>
<span ng-repeat="y in x.timetable | filter:{'name':'<?php echo $as;?>'}: true | limitTo:1">{{y.time | date:'HH:mm'}}</span>
</td>
</tr>
$as is the station to be shown. So, now the order of the list comes straight from the JSON order, so it varies a lot.
You can use the comparator as an additional argument for the filter.
So you would expand your code to:
<span ng-repeat="y in x.timetable | filter:{'name':'<?php echo $as;?>'}: true : myComparator | limitTo:1">{{y.time | date:'HH:mm'}}</span>
You need a regexp to see if the compared string values are dates and you can do something similar to:
$scope.myComparator= function (a, b) {
// regex to see if the compared values are dates
var isDate = /(-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-)/g;
// sorting dates
if (isDate.test(a.value)) {
var aDate = new Date(a.value), bDate = new Date(b.value);
return aDate.getTime() < bDate.getTime() ? -1 : 1
}
// default sorting
return a.index < b.index ? -1 : 1
}
A thing worth noting is you would need a different regular expression to find your date format. Something along the lines of the following might be sufficient:
/(\d{4})-(\d{2})-/g
NOTE: This is untested code and serves only as a guide to the right approach. Make sure your version of AngularJS supports the comparator as a filter argument.
You can order an array of objects by one of their properties with a filter function like this one:
angular.module('yourApp').filter('orderObjectBy', function() {
return function(items, field, reverse) {
var filtered = [];
angular.forEach(items, function(item) {
filtered.push(item);
});
filtered.sort(function (a, b) {
return (a[field] > b[field] ? 1 : -1);
});
if(reverse) filtered.reverse();
return filtered;
};
}
);
Use it like this
<div ng-repeat="elem in data | orderObjectBy:'number'">
// ...
</div>
I have a list of items ...
$scope.Users =[{
UserName: ''
}];
In my view I want to list them like this assuming I have only 4 items in my $scope:Users
Username1, Username2, Username3 and Username4
<span data-ng-repeat="user in Users">{{user.Username}}</span>{{$last ? '' : ', '}}
The above expression will basically add comma after each item and works fine.
My problem is how do I add an and keyword before the last item so it will be like:
Username1, Username2, Username3 and Username4
instead of:
Username1, Username2, Username3, Username4
$last is the truthy value.. so it holds either true or false and it doesn't hold the last element index..
I guess below expression should solve your problem
<p><span ng-repeat="user in Users">
{{user.Username}} {{$last ? '' : ($index==Users.length-2) ? ' and ' : ', '}}
</span></p>
Also make sure that you have the expression with $last within ng-repeat element and not outside of it
Please check the below working fiddle
http://jsfiddle.net/hrishi1183/Sek8F/2/
This could be one of the solutions
<span data-ng-repeat="user in Users">{{user.Username}}<font ng-show="!$last">,</font></span>
<span ng-repeat="user in Users">{{$first ? '' : $last ? ' and ' : ', '}}{{user.Username}}</span>
Instead of appending something prepend it. You can use $first to omit the first one. You can then use $last to add "and" instead of a comma.
Use a comma before and to promote clarity!
Bill Gates donates 6 million to Username1, Username2 and Username3.
vs
Bill Gates donates 6 million to Username1, Username2, and Username3.
Without the serial comma, the sentence does not clearly indicate that each of the users is to be given an equal share.
<span ng-repeat="user in Users">
{{user.Username}}{{$last ? '' : ($index==Username.length-2) ? ', and ' : ', '}}
</span>
Outputs:
Username1, Username2, Username3, and Username4
If you just need the text output, use a custom filter instead of ng-repeat:
<span>{{Users | humanizedUserList}}</span>
the filter code being something like this (using Lodash):
app.filter('humanizedUserList', function() {
return function(users) {
var last_users = _.last(users, 2);
return _.reduce(users, function(out, user, idx, users) {
out += user.UserName;
if(user === last_users[1]) { // last entry
return out;
}
else if(user === last_users[0]) { // second to last entry
return out + ', and ';
}
else {
return out + ', ';
}
});
};
}
You'd save yourself the hackish use of $last outside of the ng-repeat and ternary operators - and add reusability for other parts of your application.
I ran into this problem today, but with an extra challenge that the list items may need to be formatted within the sentence (for example, bold items or links). So just outputting a string from a filter wouldn't work. Initially I tried using ng-bind-html and outputting HTML from the filter, but that wasn't flexible enough.
I ended up making a directive that can be used within any ng-repeat to add the separator between list items. The tricky part is I only had access to $first, $last, and $index (not the original array or its length!).
My solution:
var app = angular.module('app', []);
app.directive('listSeparator', function() {
return {
replace: true,
template: '<span>{{ separator }}</span>',
link: function(scope, elem, attrs) {
scope.$watchGroup(["$index", "$first", "$last"], function() {
if (scope.$first)
scope.separator = "";
else if (scope.$last && scope.$index == 1)
scope.separator = " and ";
else if (scope.$last)
scope.separator = ", and ";
else
scope.separator = ",";
})
}
}
})
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.10/angular.min.js"></script>
<div ng-app="app">
Click a button to see the formatted list.
<button ng-click="myArray = ['one']">One item</button>
<button ng-click="myArray = ['one','two']">Two items</button>
<button ng-click="myArray = ['one','two','three']">Three items</button><br>
<span ng-repeat="item in myArray"><list-separator></list-separator>
<strong>{{item}}</strong></span>
</div>
Enjoy!