Vue V-for in tables - javascript

I am trying to create tables using the v-for directive, but I wanted to know if there is any way to do some conditionals inside the individual tables based on a certain value. I am including a generic version of what I have as my data and what I am trying to produce.
Data Example (this is what I am pulling in from the API call):
MainOrg
SubOrgId
SubOrgName
SubOrgState
TotalOrgs
10110
101101
Main Office
AK
26
10110
101102
Branch Office
AK
4
10110
101102
Sat Office
AK
2
10111
101111
Main Office
FL
26
10111
101112
Branch Office
FL
4
10111
101112
Sat Office
FL
2
I am trying to loop through the "MainOrg" column and create a new table for each unique one, then have the data that corresponds to that "MainOrg" in the output table. The "MainOrg" would become the title for the table.
The output I am trying to get is as follows:
10110
SubOrgId
SubOrgName
SubOrgState
TotalOrgs
101101
Main Office
AK
26
101102
Branch Office
AK
4
101102
Sat Office
AK
2
10111
SubOrgId
SubOrgName
SubOrgState
TotalOrgs
101111
Main Office
FL
26
101112
Branch Office
FL
4
101112
Sat Office
FL
2
I have been running into the following issues with the v-for and v-if directives:
**Duplicates main orgs due to it being based on index
<table v-for="(d, index) in data" :key="index">
<thead>
<tr>
<th>{{ d.MainOrg }}</th>
</tr>
</thead>
I really want it to spit out a new table for each unique "MainOrg", then contextually look at my data to include the "SubOrg" data that matches it, per my desired result above. I have not been able to find the right combination of Vue Html and/or JavaScript that can create the desired result based on the need for the data to be separated into individual tables. Also, within the table elements, I am unsure of how to reference the index of the data for conditionals. For example, when I tried using v-if instead of v-for to create the tables by accessing a unique array of the MainOrgs, I did not know how to contextually tie the data together.
In non-programmer speak/pseudo code: Take the unique MainOrg values from data and create a new table for each MainOrg. Then, take the remaining columns/rows from data and add them to each MainOrg table where the row context (data.MainOrg) matches the table for that MainOrg.
Apologies for the long post, but any help is greatly appreciated.
Edit
I am getting slightly different results from the two answers suggested as follows:
computed: {
regions: ({ estabSearchResult }) =>
estabSearchResult.reduce(
(map, { region, ...rest }) => ({
...map,
[region]: [...(map[region] ?? []), rest],
}),
{}
),
},
Some data shown is modified for sensitivity
Which gives me the following:
regions:Object
IN0110 - :Array[2]
IN0114 - :Array[1]
IN0115 - :Array[1]
IN0120 - :Array[1]
IN0130 - :Array[1]
IN0160 - :Array[1]
IN01BB - :Array[1]
IN28AO - :Array[1]
IN28BO - :Array[13]
The forEach() method(below)
if (this.estabSearchResult.length > 0) {
const newObj = {}
this.estabSearchResult.forEach(obj => {
newObj[obj.region] ?
newObj[obj.region].push(obj) : newObj[obj.region] = [obj]
})
this.estabRegionGroup = newObj
}
gives me the following:
estabRegionGroup:Object
IN0110 - :Array[3]
IN0114 - :Array[1]
IN0115 - :Array[1]
IN0120 - :Array[1]
IN0130 - :Array[1]
IN0160 - :Array[1]
IN01BB - :Array[1]
IN28AO - :Array[1]
IN28BO - :Array[13]
Notice the array size for IN0110. The forEach() gives me 3 objects in the array, where the reduce(map()) gives me only two. All other items/regions are the same and correct, only that first one is off. Any ideas? The results for IN0110 should have 3 objects in it.

Create a computed property to represent the new data structure
computed: {
regions: ({ estabSearchResult }) =>
estabSearchResult.reduce(
(map, { region, ...rest }) => ({
...map,
[region]: [...(map[region] ?? []), rest],
}),
{} // don't forget to init with an empty object
),
}
This looks something like
{
"10110": [{ SubOrgId: 101101, ... }, ...],
"10111": [{ SubOrgId: 101111, ... }, ...],
}
which you can then use in rendering
// Just some fake, random data using your "region" values
const fakeApi = {get:()=>new Promise(r=>setTimeout(r,1000,[{"region":"IN0110","foo":0.7051213449189915},{"region":"IN0110","foo":1.4213972602828675},{"region":"IN0110","foo":2.075397586536013},{"region":"IN0114","foo":0.7055750843380546},{"region":"IN0115","foo":0.5976362522109442},{"region":"IN0120","foo":0.6605446959279311},{"region":"IN0130","foo":0.7179704337235409},{"region":"IN0160","foo":0.19066097499077084},{"region":"IN01BB","foo":0.0019511615325726872},{"region":"IN28AO","foo":0.46443847116756487},{"region":"IN28BO","foo":0.41268230939585426},{"region":"IN28BO","foo":1.9572873014553014},{"region":"IN28BO","foo":2.610276341696757},{"region":"IN28BO","foo":3.7301898988777733},{"region":"IN28BO","foo":4.021495358709221},{"region":"IN28BO","foo":5.996280938563549},{"region":"IN28BO","foo":6.66092280472865},{"region":"IN28BO","foo":7.660937785660997},{"region":"IN28BO","foo":8.288195167562918},{"region":"IN28BO","foo":9.84101941796214},{"region":"IN28BO","foo":10.34450778678685},{"region":"IN28BO","foo":11.782972607317836},{"region":"IN28BO","foo":12.05067212727201}]))};
new Vue({
el: "#app",
data: () => ({
estabSearchResult: [],
}),
async created () {
this.estabSearchResult = await fakeApi.get();
},
computed: {
regions: ({ estabSearchResult }) =>
estabSearchResult.reduce(
(map, { region, ...rest }) => ({
...map,
[region]: [...(map[region] ?? []), rest],
}),
{}
),
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<p v-if="estabSearchResult.length === 0">Loading...</p>
<div v-for="(orgs, region) in regions" :key="region">
<p><strong>{{ region }} ({{ orgs.length }})</strong></p>
<table border="1">
<thead>
<tr>
<th>Foo</th>
</tr>
</thead>
<tbody>
<tr v-for="(org, index) in orgs" :key="index">
<td>{{ org.foo }}</td>
</tr>
</tbody>
</table>
</div>
</div>

First I think you have to convert your array of objects into an object of objects which contains MainOrg as key with the help of Array.forEach()
Like this :
const tableAllData = [{
MainOrg: 10110,
SubOrgId: 101101,
SubOrgName: 'Main Office'
}, {
MainOrg: 10110,
SubOrgId: 101102,
SubOrgName: 'Branch Office'
},{
MainOrg: 10110,
SubOrgId: 101102,
SubOrgName: 'Sat Office'
}, {
MainOrg: 10111,
SubOrgId: 101111,
SubOrgName: 'Main Office'
},{
MainOrg: 10111,
SubOrgId: 101112,
SubOrgName: 'Branch Office'
}, {
MainOrg: 10111,
SubOrgId: 101112,
SubOrgName: 'Sat Office'
}];
const newObj = {};
tableAllData.forEach(obj => {
newObj[obj.MainOrg] ?
newObj[obj.MainOrg].push(obj) : newObj[obj.MainOrg] = [obj]
});
console.log(newObj);
Live Demo with Vue :
new Vue({
el: '#app',
data: {
tableAllData: [{
MainOrg: 10110,
SubOrgId: 101101,
SubOrgName: 'Main Office'
}, {
MainOrg: 10110,
SubOrgId: 101102,
SubOrgName: 'Branch Office'
},{
MainOrg: 10110,
SubOrgId: 101102,
SubOrgName: 'Sat Office'
}, {
MainOrg: 10111,
SubOrgId: 101111,
SubOrgName: 'Main Office'
},{
MainOrg: 10111,
SubOrgId: 101112,
SubOrgName: 'Branch Office'
}, {
MainOrg: 10111,
SubOrgId: 101112,
SubOrgName: 'Sat Office'
}],
categorizedTable: {}
},
mounted() {
const newObj = {};
this.tableAllData.forEach(obj => {
newObj[obj.MainOrg] ?
newObj[obj.MainOrg].push(obj) : newObj[obj.MainOrg] = [obj]
});
this.categorizedTable = newObj;
}
})
table, thead, th, td, tr {
border: 1px solid black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="item in Object.keys(categorizedTable)" :key="item">
<h3>{{ item }}</h3>
<table>
<thead>
<th>SubOrgId</th>
<th>SubOrgName</th>
</thead>
<tbody>
<tr v-for="(row, index) in categorizedTable[item]" :key="index">
<td>{{ row.SubOrgId }}</td>
<td>{{ row.SubOrgName }}</td>
</tr>
</tbody>
</table>
</div>
</div>

Related

How can i display next elements in ngfor after click in table row?

I have a nested array of objects , so i am trying to display in table row only first 3 elements in array and after i am displaying remaining elements in array as a count (i.e +2).Now if i click on remain count i need to display all the elements in array on particular row click.
I am attaching the stack blitz URL for reference :- https://stackblitz.com/edit/primeng-chip-demo-agf8ey?file=src%2Fapp%2Fapp.component.html,src%2Fapp%2Fapp.component.ts
Please help me on these issue.
Thanks in advance
try this:
<p-chip
*ngFor="let cc of slice(c); let i = index"
[label]="cc.name"
></p-chip>
in .ts file
onChip(val: any) {
this.chips[val].extand = true;
}
slice(cc: any, index: number) {
if(cc?.extand) {
return cc.values;
}
return cc.values.slice(1,3);
}
also add to every object extand: false
chips = [
{
id: 1,
values: [
{
name: 'one',
},
{
name: 'two',
},
{
name: 'three',
},
{
name: 'four',
},
{
name: 'five',
},
],
extand: false
},]
Create a template variable:
<h5>Basic</h5>
<div class="p-d-flex p-ai-center">
<table>
<tr>
<th>Id</th>
<th>Chips</th>
</tr>
<tr *ngFor="let c of chips; let val = index">
<td>{{ c.id }}</td>
<td #myCell>
<ng-container *ngIf="myCell.showAll">
<p-chip *ngFor="let cc of c.values" [label]="cc.name"></p-chip>
</ng-container>
<ng-container *ngIf="!myCell.showAll">
<p-chip
*ngFor="let cc of c.values | slice: 0:3"
[label]="cc.name"
></p-chip>
<p-chip
styleClass="chipMore"
*ngIf="c.values.length >= 3"
(click)="myCell.showAll = !myCell.showAll"
>+{{ c.values.length - 3 }}</p-chip
>
</ng-container>
</td>
</tr>
</table>
</div>
Play at edited stackblitz: https://stackblitz.com/edit/primeng-chip-demo-ykmg3t?file=src/app/app.component.html

Deleting specific component in v-for array

I have below array, that contains a number of columns. Below example contains three columns, but columns can be added/removed dynamically:
[['position', '30'], ['position', '60'], ['position', '90']]
I am facing issues when deleting the correct column (index in array) with Vue.
Consider below snippet:
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
columns: [['position', '30'], ['position', '60'], ['position', '90']]
},
methods: {
deleteColumn: function(index) {
this.columns.splice(index, 1);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, index) in columns" :key="index">
Column #: {{index}} - <a #click="deleteColumn(index)">Delete me</a>
</div>
</div>
If you run the above code snippet end try to delete the #1 column, it will actually remove the #2 column (last item of the array). Same goes for #0.
I thought that by providing the index to my deleteColumn function, I could remove the "right" index from the array.
Any help is appreciated.
Just give them a property name and you are done. Notice what I changed here. Columns is no more a 2D array, but objects. Use this.$delete(this.columns, index); to delete the objects.
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
columns: {
'1': {
position: 30
},
'2': {
position: 60
},
'3': {
position: 90
}
}
},
methods: {
deleteColumn: function(index) {
this.$delete(this.columns, index);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, index) in columns" :key="index">
Column #: {{index}} - <a #click="deleteColumn(index)">Delete me</a>
</div>
</div>
{
'1': {
position: 30
},
'2': {
position: 60
},
'3': {
position: 90
}
}
Here, '1' is a property name and it's value is another object. It's like giving ids to your data.
The format for value of object is this
{ property_name : value }
Here, value is another object, and in that object, there is another property, named "position" with your corresponding values.
When you clicked any item you are removing it in the right way, your index is your key, that's the problem, but is visually, in the logic it's right. Display your position in your template just for you can see it. ANd for me your data it's not in the right way.
<div id="app">
<div v-for="(item, index) in columns" :key="index">
Column #: {{index}}-{{item.position}} -
<a #click="deleteColumn(index)">Delete me</a>
</div>
</div>
and your script for you can see the change
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
columns: [{position: 30}, {position: 60}, {position: 90}]
},
methods: {
deleteColumn: function(index) {
this.columns.splice(index, 1);
}
}
})
The splice method reindexes the array, moving all elements after the splice point up or down so that any new inserted values will fit and so that the array indices remain contiguous. You can see it more clearly if you also display the values of the items in your list:
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
columns: ['foo', 'bar', 'baz']
},
methods: {
deleteColumn: function(index) {
this.columns.splice(index, 1);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, index) in columns" :key="index">
Column #{{index}} = {{item}} - <a #click="deleteColumn(index)" style="cursor:pointer">Delete me</a>
</div>
</div>
Initially, the snippet above will render like this:
Column #0 = foo - Delete me
Column #1 = bar - Delete me
Column #2 = baz - Delete me
If you now click the "Delete me" link on column #0 ("foo"), it will change to:
Column #0 = bar - Delete me
Column #1 = baz - Delete me
You can see that the value "foo" indeed got spliced out of the array — and the values "bar" and "baz" were shifted down by one position to become the new elements #0 and #1.
Anyway, the fix for this problem is simply "don't do that":
If you're using v-for with a simple array whose elements have no natural key value, you can just omit :key entirely and let Vue decide how to best handle changes to the underlying array. As long as the contents within the v-for loop doesn't contain any form inputs or stateful components or other fancy stuff that doesn't react well to the array being reindexed, it should work just fine.
Conversely, if you do have a natural unique key available for each array element, use it. If you don't, but can create one, consider doing that.
You should not use index as the key with CRUD operations since this will confuse Vue when it comes to deleting. The key should be a unique identifier that relates to the data.
You can create a new formatted array of objects on mount with a key generated from the data within the array (note: I haven't tested the code in a browser if there are any mistakes).
<template>
<div>
<div v-for="col in formattedColumns" :key="col.key">
{{ col.value }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
columns: [['position', '30'], ['position', '60'], ['position', '90']],
formattedColumns: null,
};
},
mounted() {
let columns = [];
for (let i = 0; i < this.columns.length; i++) {
columns.push({
value: this.columns[i],
key: this.columns[i][0] + this.columns[i][1],
});
}
this.formattedColumns = columns;
},
};
</script>
Try this this.$delete(this.columns, index) which is the same as Vue.delete(this.columns, index)
https://v2.vuejs.org/v2/api/index.html#Vue-delete

How would I create a timesheet and tie each input value to the respective date?

I wish to create a timesheet grid. Basically, a grid that displays the current week and the project you're working on as such: https://jsfiddle.net/ho9a8neq/
How do I set up v-model so I can correctly send the value with the corresponding date to a database?
Something like
[
{date: "jan-1": 8, project: 1},
{date: "jan-2": 10, project: 1},
{date: "jan-3": 10, project: 2}
]
To be able to collect data for different projects during a timeline and track the day of each one the solution I propose consists in the following data structure:
day = ''
week = [day, day, day]
project = [week, week, ...]
For the sake of simplicity it limits to adding future weeks. Its possible to change this, but it will require more complexity on the models and I don't think it will help to understand how to bind data to the model.
For each week there must be a model that retains each day data, this is done creating an array of empty strings:
week: ['','','','','','','']
Each project can contain several weeks:
data: [week, week, week]
When the user creates a new project it must replicate the project model considering the current week:
_.cloneDeep(project(this.weekNum, this.rows.length))
Now, with the data structure in place, bind the view to it:
<input type="text" style="width: 35px;" v-model="row.data[weekNum][i]">
See snippet to understand how everything ties together:
const weekData = () => new Array(7).fill('');
const project = (weekNum, id) => ({
project: "first",
id,
data: Array(weekNum + 1).fill([]).map(weekData)
});
new Vue({
el: "#app",
data: {
weekNum: 0,
rows: [_.cloneDeep(project(0, 0))]
},
methods: {
addProject() {
window.pp = _.cloneDeep(
project(this.weekNum, this.rows.length)
)
this.rows.push(
window.pp
);
},
deleteRow(key) {
this.rows.splice(key, 1);
},
nextWeek() {
this.weekNum++;
this.rows.forEach(_project => {
if (!_project.data[this.weekNum]) {
_project.data.push(weekData());
}
});
},
prevWeek() {
this.weekNum--;
this.rows.forEach(row => {
if (!row.data[this.weekNum]) {
row.data.unshift(weekData());
}
});
},
dates(dateFormat, weekNumber) {
let startOfWeek = moment().startOf('week').add(weekNumber, "week");
const endOfWeek = moment().endOf('week').add(weekNumber, "week");
const days = [];
while (startOfWeek <= endOfWeek) {
days.push(startOfWeek.format(dateFormat))
startOfWeek = startOfWeek.clone().add(1, 'd');
}
return days
},
log() {
const output = _.reduce(this.rows, (result, row) => {
const project = {
project: row.id
};
const startWeek = moment().startOf('week');
const weekDays = [];
row.data.forEach((week, weekIdx) => {
week.forEach((data, dataIdx) => {
if (data === '') return;
weekDays.push({
data,
project: row.id,
day: startWeek.clone().add(weekIdx, 'week').add(dataIdx, 'd').format('MMM D')
});
});
});
return [...result, ...weekDays];
}, []);
console.log(output)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/moment#2.24.0/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.11/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.22/dist/vue.min.js"></script>
<div id="app">
<!-- for the sake of simplicity limit the date to future weeks -->
<button #click="prevWeek" :disabled="weekNum < 1">Previous week</button>
<button #click="nextWeek">Next week</button>
<button #click="addProject">Add project</button>
<table>
<tr>
<th>Project</th>
<th v-for="(day, i) in dates('MMM D', weekNum)" :key="i">{{day}}</th>
</tr>
<tbody>
<tr v-for="(row, key) in rows" :key="key">
<td>Project {{key}}</td>
<td v-for="(n,i) in dates('YYYY-MM-DD', weekNum)" :key="`${row.id}-${i}`">
<input type="text" style="width: 35px;" v-model="row.data[weekNum][i]">
</td>
<td><button #click="deleteRow(key)">x</button></td>
</tr>
</tbody>
</table>
<button #click="log()">log</button>
</div>

Uncaught ReferenceError with a seemingly defined variable

So I have this template that utilizes KendoUI to render a grid. Here's a part of it:
<script id="rowTemplateCourse" type="text/x-kendo-tmpl">
<tr data-cid="#: id #" class="course-row" id="course-row#: id #">
<td>
<span class="circle-indicator label-#if(package_is_active == 1){#success#}else{#danger#}#"></span>
</td>
<td>
#: course_name # - #= name#
</td>
<td>
<span class="badge element-bg-color-blue">ver. #:version_number#</span>
</td>
</tr>
</script>
I get the needed information from a php controller, that loads a variable in my view that holds this template. Variable holds this sort of data:
[1] => Array(
[id] => 544
[course_name] => Course for whatever
[price] => 52
[logo] => assets/images/new_course.png
[version_number] => 1
[parent_version_id] => 0
[course_price] => 52.00
[description_for_school] =>
[is_print_only] => 0
[offer_pdf] => 0
[pdf_final_price] => 0.00
[simple_course] => 0
[state_id] => 50
[name] => Tennessee
[cs_days_to_complete] => 120
[course_is_active] => 1
[user_in_course] => no
[user_is_waiting] => no
[days_to_complete] => 0)
In my view, I parse this variable like so:
var course_data = JSON.parse('<?php print(json_encode($courses));?>');
This works correctly and returns the same data like so (copy from console.log):
1: Object
course_is_active:"1"
course_name:"Course for whatever"
course_price:"52.00"
cs_days_to_complete:"120"
days_to_complete:0
description_for_school:""
id:"544"
is_print_only:"0"
logo:"assets/images/new_course.png"
name:"Tennessee"
offer_pdf:"0"
parent_version_id:"0"
pdf_final_price:"0.00"
price:"52"
simple_course:"0"
state_id:"50"
user_in_course:"no"
user_is_waiting:"no"
version_number: "1"
I load the data in a grid like so:
var courses_grid = $("#courses_grid").kendoGrid({
dataSource: {
data: course_data,
schema: {
model: {
fields: {
id: {
type: "number"
},
course_name: {
type: "string"
},
course_short_description: {
type: "string"
}
}
}
},
pageSize: 10,
},
toolbar: kendo.template($("#course-header-template").html()),
rowTemplate: kendo.template($("#rowTemplateCourse").html()),
groupable: false,
sortable: true,
selectable: "single",
pageable: {
refresh: true,
pageSizes: true,
buttonCount: 5
},
columns: [{
title: "Status",
width: 100
}, {
title: "Course Name",
}]
});
When the page loads, I get an error that course_is_active is not defined. I don't see how it's not defined, as it is clearly here and has a value. Can someone help me figure this out?
More info on the error:
Uncaught ReferenceError: course_is_active is not defined
(function(data
/**/) {
var $kendoOutput, $kendoHtmlEncode = kendo.htmlEncode;with(data){$kendoOutput='\n\t <tr data-cid="'+$kendoHtmlEncode( id )+'" class="course-row" id="course-row'+$kendoHtmlEncode( id )+'">\n <td>\n <span class="circle-indicator label-';if(course_is_active == 1){;$kendoOutput+='success';}else{;$kendoOutput+='danger';};$kendoOutput+='"></span>\n </td>\n\t\t <td>\n '+$kendoHtmlEncode( course_name )+' - '+( name)+'\n\t\t </td>\n\t\t\t<td>\n <span class="badge element-bg-color-blue">ver. '+$kendoHtmlEncode(version_number)+'</span>\n\t\t </td>\n\t </tr>\n\n';}return $kendoOutput;
})
I have found the problem. In my PHP code, I am checking in the arrays if a value is equal to 0 and if it is, I am removing that element from the array. This happens to be the first element in the 2D array I load in the view, so when KendoUI starts to load variables in the table, it starts with the [0] index, that does not exist, and throws an error. Thanks to everyone that participated.

apply formatting filter dynamically in a ng-repeat

My goal is to apply a formatting filter that is set as a property of the looped object.
Taking this array of objects:
[
{
"value": "test value with null formatter",
"formatter": null,
},
{
"value": "uppercase text",
"formatter": "uppercase",
},
{
"value": "2014-01-01",
"formatter": "date",
}
]
The template code i'm trying to write is this:
<div ng-repeat="row in list">
{{ row.value | row.formatter }}
</div>
And i'm expecting to see this result:
test value with null formatter
UPPERCASE TEXT
Jan 1, 2014
But maybe obviusly this code throws an error:
Unknown provider: row.formatterFilterProvider <- row.formatterFilter
I can't immagine how to parse the "formatter" parameter inside the {{ }}; can anyone help me?
See the plunkr http://plnkr.co/edit/YnCR123dRQRqm3owQLcs?p=preview
The | is an angular construct that finds a defined filter with that name and applies it to the value on the left. What I think you need to do is create a filter that takes a filter name as an argument, then calls the appropriate filter (fiddle) (adapted from M59's code):
HTML:
<div ng-repeat="row in list">
{{ row.value | picker:row.formatter }}
</div>
Javascript:
app.filter('picker', function($filter) {
return function(value, filterName) {
return $filter(filterName)(value);
};
});
Thanks to #karlgold's comment, here's a version that supports arguments. The first example uses the add filter directly to add numbers to an existing number and the second uses the useFilter filter to select the add filter by string and pass arguments to it (fiddle):
HTML:
<p>2 + 3 + 5 = {{ 2 | add:3:5 }}</p>
<p>7 + 9 + 11 = {{ 7 | useFilter:'add':9:11 }}</p>
Javascript:
app.filter('useFilter', function($filter) {
return function() {
var filterName = [].splice.call(arguments, 1, 1)[0];
return $filter(filterName).apply(null, arguments);
};
});
I like the concept behind these answers, but don't think they provide the most flexible possible solution.
What I really wanted to do and I'm sure some readers will feel the same, is to be able to dynamically pass a filter expression, which would then evaluate and return the appropriate result.
So a single custom filter would be able to process all of the following:
{{ammount | picker:'currency:"$":0'}}
{{date | picker:'date:"yyyy-MM-dd HH:mm:ss Z"'}}
{{name | picker:'salutation:"Hello"'}} //Apply another custom filter
I came up with the following piece of code, which utilizes the $interpolate service into my custom filter. See the jsfiddle:
Javascript
myApp.filter('picker', function($interpolate ){
return function(item,name){
var result = $interpolate('{{value | ' + arguments[1] + '}}');
return result({value:arguments[0]});
};
});
One way to make it work is to use a function for the binding and do the filtering within that function. This may not be the best approach: Live demo (click).
<div ng-repeat="row in list">
{{ foo(row.value, row.filter) }}
</div>
JavaScript:
$scope.list = [
{"value": "uppercase text", "filter": "uppercase"}
];
$scope.foo = function(value, filter) {
return $filter(filter)(value);
};
I had a slightly different need and so modified the above answer a bit (the $interpolate solution hits the same goal but is still limited):
angular.module("myApp").filter("meta", function($filter)
{
return function()
{
var filterName = [].splice.call(arguments, 1, 1)[0] || "filter";
var filter = filterName.split(":");
if (filter.length > 1)
{
filterName = filter[0];
for (var i = 1, k = filter.length; i < k; i++)
{
[].push.call(arguments, filter[i]);
}
}
return $filter(filterName).apply(null, arguments);
};
});
Usage:
<td ng-repeat="column in columns">{{ column.fakeData | meta:column.filter }}</td>
Data:
{
label:"Column head",
description:"The label used for a column",
filter:"percentage:2:true",
fakeData:-4.769796600014472
}
(percentage is a custom filter that builds off number)
Credit in this post to Jason Goemaat.
Here is how I used it.
$scope.table.columns = [{ name: "June 1 2015", filter: "date" },
{ name: "Name", filter: null },
] etc...
<td class="table-row" ng-repeat="column in table.columns">
{{ column.name | applyFilter:column.filter }}
</td>
app.filter('applyFilter', [ '$filter', function( $filter ) {
return function ( value, filterName ) {
if( !filterName ){ return value; } // In case no filter, as in NULL.
return $filter( filterName )( value );
};
}]);
I improved #Jason Goemaat's answer a bit by adding a check if the filter exists, and if not return the first argument by default:
.filter('useFilter', function ($filter, $injector) {
return function () {
var filterName = [].splice.call(arguments, 1, 1)[0];
return $injector.has(filterName + 'Filter') ? $filter(filterName).apply(null, arguments) : arguments[0];
};
});
The newer version of ng-table allows for dynamic table creation (ng-dynamic-table) based on a column configuration. Formatting a date field is as easy as adding the format to your field value in your columns array.
Given
{
"name": "Test code",
"dateInfo": {
"createDate": 1453480399313
"updateDate": 1453480399313
}
}
columns = [
{field: 'object.name', title: 'Name', sortable: 'name', filter: {name: 'text'}, show: true},
{field: "object.dateInfo.createDate | date :'MMM dd yyyy - HH:mm:ss a'", title: 'Create Date', sortable: 'object.dateInfo.createDate', show: true}
]
<table ng-table-dynamic="controller.ngTableObject with controller.columns" show-filter="true" class="table table-condensed table-bordered table-striped">
<tr ng-repeat="row in $data">
<td ng-repeat="column in $columns">{{ $eval(column.field, { object: row }) }}</td>
</tr>
</table>
I ended up doing something a bit more crude, but less involving:
HTML:
Use the ternary operator to check if there is a filter defined for the row:
ng-bind="::data {{row.filter ? '|' + row.filter : ''}}"
JS:
In the data array in Javascript add the filter:
, {
data: 10,
rowName: "Price",
months: [],
tooltip: "Price in DKK",
filter: "currency:undefined:0"
}, {
This is what I use (Angular Version 1.3.0-beta.8 accidental-haiku).
This filter allows you to use filters with or without filter options.
applyFilter will check if the filter exists in Angular, if the filter does not exist, then an error message with the filter name will be in the browser console like so...
The following filter does not exist: greenBananas
When using ng-repeat, some of the values will be undefined. applyFilter will handle these issues with a soft fail.
app.filter( 'applyFilter', ['$filter', '$injector', function($filter, $injector){
var filterError = "The following filter does not exist: ";
return function(value, filterName, options){
if(noFilterProvided(filterName)){ return value; }
if(filterDoesNotExistInAngular(filterName)){ console.error(filterError + "\"" + filterName + "\""); return value; }
return $filter(filterName)(value, applyOptions(options));
};
function noFilterProvided(filterName){
return !filterName || typeof filterName !== "string" || !filterName.trim();
}
function filterDoesNotExistInAngular(filterName){
return !$injector.has(filterName + "Filter");
}
function applyOptions(options){
if(!options){ return undefined; }
return options;
}
}]);
Then you use what ever filter you want, which may or may not have options.
// Where, item => { name: "Jello", filter: {name: "capitalize", options: null }};
<div ng-repeat="item in items">
{{ item.name | applyFilter:item.filter.name:item.filter.options }}
</div>
Or you could use with separate data structures when building a table.
// Where row => { color: "blue" };
// column => { name: "color", filter: { name: "capitalize", options: "whatever filter accepts"}};
<tr ng-repeat="row in rows">
<td ng-repeat="column in columns">
{{ row[column.name] | applyFilter:column.filter.name:column.filter.options }}
</td>
</tr>
If you find that you require to pass in more specific values you can add more arguments like this...
// In applyFilter, replace this line
return function(value, filterName, options){
// with this line
return function(value, filterName, options, newData){
// and also replace this line
return $filter(filterName)(value, applyOptions(options));
// with this line
return $filter(filterName)(value, applyOptions(options), newData);
Then in your HTML perhaps your filter also requires a key from the row object
// Where row => { color: "blue", addThisToo: "My Favorite Color" };
// column => { name: "color", filter: { name: "capitalize", options: "whatever filter accepts"}};
<tr ng-repeat="row in rows">
<td ng-repeat="column in columns">
{{ row[column.name] | applyFilter:column.filter.name:column.filter.options:row.addThisToo }}
</td>
</tr>

Categories

Resources