How to render tr conditionally in knockout.js foreach binding - javascript

I would like to render dynamically rows and columns using knockout. The idea is that I would like to populate each row with some cells and dynamically add more rows if needed.
lets assume that totall number of cells equals 4*number of rows, then I tried:
<table>
<tbody data-bind="foreach: model">
<!--ko if: $index() % 4 == 0--><tr><!--/ko-->
<td>
<label data-bind="text: Value"></label>
</td>
<td>
<input type="checkbox" data-bind="checked: IsChecked"/>
</td>
<!--ko if: $index() % 4 == 0--></tr><!--/ko-->
</tbody>
</table>
but it works like it was:
<table>
<tbody data-bind="foreach: model">
<!--ko if: $index() % 4 == 0-->
<td>
<label data-bind="text: Value"></label>
</td>
<td>
<input type="checkbox" data-bind="checked: IsChecked"/>
</td>
</tr><!--/ko-->
</tbody>
</table>
by not rendering whole row with content, is it possible with knockout to render all cells and add rows only when needed?
As a workaround I thinking about nested foreach, but it would require my model to change from single dimensional to two dimensional which seems odd.

Add another computed property that structures your data into rows:
<table>
<tbody data-bind="foreach: rows">
<tr>
<!-- ko foreach: $data -->
<td data-bind="text:$index"></td>
<td data-bind="text:fname"></td>
<td data-bind="text:lname"></td>
<!-- /ko -->
</tr>
</tbody>
</table>
with code:
var vm = {
people: ko.observableArray([
{ fname: 'fname', lname: 'lname' },
{ fname: 'fname', lname: 'lname' },
{ fname: 'fname', lname: 'lname' },
{ fname: 'fname', lname: 'lname' }
])
};
vm.rows = ko.computed(function () {
var itemsPerRow = 3, rowIndex = 0, rows = [];
var people = vm.people();
for (var index = 0; index < people.length; index++) {
if (!rows[rowIndex])
rows[rowIndex] = [];
rows[rowIndex].push(people[index]);
if (rows[rowIndex].length == itemsPerRow)
rowIndex++;
}
return rows;
}, vm);
$(function () {
ko.applyBindings(vm);
});

Your syntax will not work with knockout default templating engine just because it uses DOM.
If you need to do this, use string-based external templating engine (it will treat your template as string and will use regex and string manipulations, so you will be able to do this trick with conditional rendering of start/end tag).
Your example using underscore js:
http://jsfiddle.net/2QKd3/5/
HTML
<h1>Table breaking</h1>
<ul data-bind="template: { name: 'peopleList' }"></ul>
<script type="text/html" id="peopleList">
<table>
<tbody>
{{ _.each(model(), function(m, idx) { }}
{{ if (idx % 4 == 0) { }}
<tr>
{{ } }}
<td>
<label>{{= m.Value }}</label>
</td>
<td>
<input type="checkbox" data-bind="checked: m.IsChecked"/>
</td>
{{ if (idx % 4 == 3) { }}
</tr>
{{ } }}
{{ }) }}
</tbody>
</table>
</script>
Javascript (this includes underscore integration decribed here - http://knockoutjs.com/documentation/template-binding.html
_.templateSettings = {
interpolate: /\{\{\=(.+?)\}\}/g,
evaluate: /\{\{(.+?)\}\}/g
};
/* ---- Begin integration of Underscore template engine with Knockout. Could go in a separate file of course. ---- */
ko.underscoreTemplateEngine = function () { }
ko.underscoreTemplateEngine.prototype = ko.utils.extend(new ko.templateEngine(), {
renderTemplateSource: function (templateSource, bindingContext, options) {
// Precompile and cache the templates for efficiency
var precompiled = templateSource['data']('precompiled');
if (!precompiled) {
precompiled = _.template("{{ with($data) { }} " + templateSource.text() + " {{ } }}");
templateSource['data']('precompiled', precompiled);
}
// Run the template and parse its output into an array of DOM elements
var renderedMarkup = precompiled(bindingContext).replace(/\s+/g, " ");
return ko.utils.parseHtmlFragment(renderedMarkup);
},
createJavaScriptEvaluatorBlock: function(script) {
return "{{ " + script + " }}";
}
});
ko.setTemplateEngine(new ko.underscoreTemplateEngine());
/* ---- End integration of Underscore template engine with Knockout ---- */
var viewModel = {
model: ko.observableArray([
{ Value: '1', IsChecked: 1 },
{ Value: '2', IsChecked: 0 },
{ Value: '3', IsChecked: 1 },
{ Value: '4', IsChecked: 0 },
{ Value: '5', IsChecked: 1 },
])
};
ko.applyBindings(viewModel);
P.S.: but better avoid using tables for html layout. Your example can be rendered using inline-block elements with much cleaner code.

Related

ng-table number range filter is not working

what I am trying to do
I am trying to make one of my columns to take two number (in one column), so I can filter the numeric data by range just for this column. All other sort and pagination and filter by 'contain text' is working fine but I am just not sure how would I go about making just one particular column to have 'range' filter.
graphical representation of what I want
column_header1 columns_header2 column_header3
contain_filter1 contain_filter2 filter3_min_number
filter3_max_number
data data numeric data
. . .
. . .
. . .
What I have so far
I found one example from ng-table module website and I tried to implement their code to mine but I don't know how to approach it when I have to implement the range function inside my 'getData'.
Example that I found http://codepen.io/christianacca/pen/yNWeOP
The custom filter algorithm on 'age' data was what I was looking at.
my app.js
$http.get("http://hostname:port/variant/whole/1-10000", {cache: true})
.then(function (response) {
$scope.variants = response.data;
$scope.data = $scope.variants;
var self = this;
self.variantFilterDef = {
start: {
id: 'number',
placeholder: 'Start'
},
end: {
id: 'number',
placeholder: 'End'
}
};
self.variantsTable = new NgTableParams({
page:1,
count: 10
}, {
filterOptions: { filterFn: variantRangeFilter },
dataset: $scope.data,
filterLayout: "horizontal"
});
function variantRangeFilter(data, filterValues/*, comparator*/){
return data.filter(function(item){
var start = filterValues.start == null ? Number.MIN_VALUE : filterValues.start;
var end = filterValues.end == null ? Number.MAX_VALUE : filterValues.end;
return start <= item.pos && end >= item.pos;
});
}
/* from this part on, it is working code but no 'Range' function
$scope.variantsTable = new NgTableParams({
page: 1,
count: 10
}, {
total: $scope.variants.length,
getData: function (params) {
if (params.sorting()) {
$scope.data = $filter('orderBy')($scope.variants, params.orderBy());
} else {
$scope.data = $scope.variants;
}
if (params.filter()) {
$scope.data = $filter('filter')($scope.data, params.filter());
} else {
$scope.data = $scope.data;
}
$scope.data = $scope.data.slice((params.page() - 1) * params.count(), params.page() * params.count());
return $scope.data;
}
});
*/
});
});
my variant.html
<table ng-table="variantsTable" show-filter="true" class="table table-bordered table-striped table-condensed">
<tr ng-repeat="variant in $data">
<td data-title="'chrom'" sortable="'chrom'" filter="{ 'chrom': 'text' }" >
{{variant.chrom}}
</td>
<td data-title="'id'" sortable="'id'" filter="{ 'id': 'text' }" >
{{variant.id}}
</td>
<td data-title="'pos'" sortable = "'pos'" filter = "{ 'pos': 'text' }">
{{variant.pos}}
</td>
I would really appreciate any suggestion or any input, thanks!
The filter attribute of the ID table cell is not correct.
<td data-title="'id'" sortable="'id'" filter="{ 'id': 'text' }">
{{variant.id}}
</td>
Change it to:
<td data-title="'id'" sortable="'id'" filter="variantFilterDef">
{{variant.id}}
</td>
EDIT
After a bit of trial and error I have it working. I started from your code sample and made a number of changes. I used ControllerAs syntax. But essentially the fixes are:
<td data-title="'chrom'" sortable="'chrom'" filter="{ 'chrom': 'text' }">
to <td data-title="'chrom'" sortable="'chrom'" filter="{ 'name': 'text' }">
<td data-title="'pos'" sortable = "'pos'" filter = "{ 'pos': 'text' }">
to <td data-title="'pos'" sortable="'pos'" filter="variantCtrl.variantFilterDef">
if (params.filter()) {
self.data = $filter('filter')(self.data, {name: params.filter().name});
self.data = variantRangeFilter(self.data, params.filter());
} else {
self.data = self.data;
}
The main issue was the need to separate out the filters of the two columns in #3 by using {name: params.filter().name}) & then calling the custom Pos filter separately.
Codepen: http://codepen.io/anon/pen/QKBYOq?editors=1011

Rendering a table with dynamic headers

I have to render a table with dynamic headers, I mean, I don't want to do something like this in the HTML
<table>
<tr>
// THIS TABLE ROW IS WHAT I SAY
<th>here info</th>
<th>something here</th>
<th>another header</th>
</tr>
<tr ng-repeat="thing in things">
<td>{{thing.asfs}}</td>
<td>{{thing.asx}}</td>
<td>{{person.dsf}}</td>
</tr>
</table>
I want something like this
<table>
<tr ng-repeat="head in heads">
{{head}}
</tr>
<tr ng-repeat="bar in bars">
<td ng-repeat="foo in foos"></td>
</tr>
</table>
that is only an example, I need to do it with this data:
{
"55f6de98f0a50c25f7be4db0":{
"clicks":{
"total":144,
"real":1
},
"conversions":{
"total":4,
"amount":229
},
"cost":{
"cpc":0.1999999999999995,
"ecpc":1145.0000000000027,
"total":28.79999999999993
},
"revenue":{
"total":4,
"epc":0.027777777777777776
},
"net":{
"roi":-1.1612903225806457,
"total":4
},
"name":"Traffic Source #2",
},
"55f6de98f0a50c25f7be4dbOTHER":{
"clicks":{
"total":144,
"real":1
},
"conversions":{
"total":4,
"amount":229
},
"cost":{
"cpc":0.1999999999999995,
"ecpc":1145.0000000000027,
"total":28.79999999999993
},
"revenue":{
"total":4,
"epc":0.027777777777777776
},
"net":{
"roi":-1.1612903225806457,
"total":4
}
"name":"Traffic Source #3"
},
}
every key, like clicks, conversions, cost, etc, should be a td, it is just that I don't want static HTML.
Any suggestions?
EDIT
And also, sometimes that object will grow, could come up with some more keys like this one 55f6de98f0a50c25f7be4db0
I did this fiddle with the exact same data I am receiving
http://jsfiddle.net/wLkz45qj/
UPDATE:
What you need to do is first convert you inconvenient object to array of objects with simple structure, and then use my code , i.e.
{
a: {
b:{
c: 'x'
}
}
}
will turn into
[[ a, { 'b.c' : 'x' }], ...]
or just
[{ _id : a , 'b.c' :'x'}, ...]
easiest way to do that is to use lodash or underscore ( check map, flatMap, pairs etc)
#jperezov showed you core idea, little bit detailed example:
$scope.peopleKeys = Object.keys(people[0])
and
<table>
<tr>
<th></th>
<th ng-repeat="personKey in peopleKeys">
{{ personKey }}
</th>
</tr>
<tr ng-repeat='p in people'>
<th>{{ $index }}</th>
<td ng-repeat="personKey in peopleKeys">
{{ p[personKey] }}
</td>
</tr>
</table>
You may also have some dictionary with display names:
$scope.displayNames = {
id: 'ID',
firstName: 'First Name'
...
}
and then your header going to be:
<tr>
<th></th>
<th ng-repeat="personKey in peopleKeys">
{{ displayNames[personKey] }}
</th>
</tr>
PS: OR you can just use ui-grid
var app = angular.module('myApp', []);
function PeopleCtrl($scope, $http) {
$scope.headers=[];
$scope.data = [];
$scope.LoadMyJson = function() {
for (var s in myJson){
$scope.data.push(s);
if ($scope.headers.length < 1)
for (var prop in myJson[s]){
prop.data = [];
$scope.headers.push({th:prop, td: []});
}
}
for (var s in $scope.data){
for (var prop in $scope.headers){
var header = $scope.headers[prop].th;
var data = myJson[$scope.data[s]][header];
$scope.headers[prop].td.push(data);
}
}
};
}
What you're looking for is something like this, I think:
http://jsfiddle.net/wLkz45qj/8/
Maybe iterate another time over "inner" for formatting.

How to target the last column in the table body to replace its value with a fixed value for every row?

Using Vue, I have displayed table with dynamic data pulled from external JSON.
I want to target the last column in the table body to replace its value with a fixed value for every row.
How would I do this?
Note that my script uses the initial value from the JSON data for that column to determine which class to put on that td.
Here is my code:
var dataURL = 'inc/data.json.php'
Vue.component('demo-grid', {
template: '#grid-template',
replace: true,
props: ['data', 'columns', 'filter-key'],
data: function() {
return {
data: null,
columns: null,
sortKey: '',
filterKey: '',
reversed: {}
}
},
compiled: function() {
// initialize reverse state
var self = this
this.columns.forEach(function(key) {
self.reversed.$add(key, false)
})
},
methods: {
sortBy: function(key) {
this.sortKey = key
this.reversed[key] = !this.reversed[key]
}
}
})
var demo = new Vue({
el: '#app',
data: {
searchQuery: '',
gridColumns: [...],
gridData: []
},
ready: function() {
this.fetchData()
},
methods: {
fetchData: function() {
var xhr = new XMLHttpRequest(),
self = this
xhr.open('GET', programsURL)
xhr.onload = function() {
self.gridData = JSON.parse(xhr.responseText)
}
xhr.send()
}
}
})
<table>
<thead>
<tr>
<th v-repeat="key: columns" v-on="click:sortBy(key)" v-class="active: sortKey == key">
{{key | capitalize}}
<span class="arrow" v-class="reversed[key] ? 'dsc' : 'asc'">
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-repeat="
entry: data
| filterBy filterKey
| orderBy sortKey reversed[sortKey]">
<!-- here is where I wish to target the 5th in this row to change its value -->
<td v-repeat="key: columns" v-class="lvl-1 : entry[key] === '1', lvl-2 : entry[key] === '2', lvl-3 : entry[key] === '3'>
{{entry[key]}}
</td>
</tr>
</tbody>
</table>
Compare the special $index property with the length of the array (or computed property), and then use a template fragment so you can switch out the <td>
<template v-repeat="column in columns">
<td v-show="$index < columns.length-1">All other columns...</td>
<td v-show="$index === columns.length-1">Last Column</td>
</template>
Solved it with:
<div v-if="$index === 4">
...

AngularJS: How to parse JSON string to integer or float to display it in form number field

I am using AngularJS for my front-end and Laravel for the back-end. I am passing a variable to front-end screen called orderItems which contains the following data to my front-end.
[{
"item_description": "CAD Machine ",
"qty": "1",
"unit_price": "4000.00"
}, {
"item_description": "Lenovo laptop",
"qty": "1",
"unit_price": "3000.00"
}]
My front-end contains one text field and two number fields. Data is getting populated on text field and no data is showing up on number field.
Here is my code from the view:
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>Description</th>
<th>Quantity</th>
<th>Cost</th>
<th>Total</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in order.items">
<td>
{{ Form::text("description[]",null, ['ng-model' => 'item.item_description', 'class' => 'input-small form-control', 'placeholder' => 'Item description', 'required']) }}
</td>
<td>
{{ Form::number("quantity[]", null, ['step' => 'any', 'ng-model' => 'item.qty', 'class' => 'input-mini form-control', 'placeholder' => 'Qty']) }}
</td>
<td>
{{ Form::number("cost[]", null, ['step' => 'any', 'ng-model' => 'item.unit_price', 'class' => 'input-mini form-control', 'placeholder' => 'Unit Price', 'required']) }}
</td>
<td>
<h4><% item.qty * item.cost | currency %></h4>
</td>
<td class="text-center">
<h5><a href ng-click="removeItem($index)"><i class="fa fa-times"></i> Remove</a></h5>
</td>
</tr>
</tbody>
</table>
Here is my controller where I assign orderItems to the scope element
app.controller('POEditFormController', function($scope) {
$scope.order = {
items: <?= $orderItems ?>
};
$scope.addItem = function() {
var len = $scope.order.items.length;
var prevItem = $scope.order.items[len - 1];
if (prevItem.item_description == "" || prevItem.unit_price == "") {
alert("Please enter the line details.");
} else {
$scope.order.items.push({
qty: 1,
item_description: '',
unit_price: ''
});
}
},
$scope.removeItem = function(index) {
$scope.order.items.splice(index, 1);
},
$scope.total = function() {
var total = 0;
angular.forEach($scope.order.items, function(item) {
total += item.qty * item.unit_price;
})
return total;
}
});
Because of my limited knowledge, I am not sure how to parse the qty and unit_price to float/integer.
You don't need to parse them in this case.
In general, Javascript is interpreted, so in most browsers if you do the following:
"3.5"*"2"
It will work.
However, if for some reasons you need to force the parsing, you can use:
parseFloat
parseInt
In
total += parseInt(item.qty) * parseFloat(item.unit_price);
These are two functions which respectively parse the string and return the parsed value.
These two are the most reliable functions. There are others possibilities, such as:
+ before the number (such as +"3.5")
Integer.parseInt(myNumber)
But these are not recognized in every browser
#wayne thanks for the suggestion, I managed to change the server response by setting up the accessors on the model class for qty and unit_price
public function getQtyAttribute($value)
{
return $this->attributes['qty'] = (float)$value;
}
public function getUnitPriceAttribute($value)
{
return $this->attributes['unit_price'] = (float)$value;
}
Here is the link to Laravel's Eloquent Accessors & Mutators

Focus input added dynamically to observableArray in knockout

In KO 3.0.0.beta (and I am almost sure it'd be the same in 2.3) I am trying to add new row to a dynamically created table:
HTML
<div id="root">
<table>
<tbody data-bind="foreach: events">
<tr>
<td>
<input type="text" data-type="name" data-bind="value: name, css: {hidden: !editing()}, hasFocus: true">
</td>
<td>
<input type="text" data-type="method" data-bind="value: age, css: {hidden: !editing()}">
</td>
</tr>
</tbody>
</table>
</div>
JavaScript
var $root = $('#root');
$root.on('blur', 'table input', function(event) {
var data = ko.dataFor(event.target),
context = ko.contextFor(event.target),
$input = $(event.target),
newData;
data.editing(false);
if($input.closest('td').is(':last-child') && $input.closest('tr').is(':last-child')) {
newData = {
name: '',
age: '',
editing: ko.observable(false)
};
context.$root.events.push(newData);
newData.editing(true);
}
});
var data = {
events: [
{name: 'aaa', age: '22', editing: false},
{name: 'bbb', age: '33', editing: false},
{name: 'ccc', age: '44', editing: false}
]
};
var mapping = {
events: {
key: function(data) {
return ko.unwrap(data.name);
}
}
};
var observable = ko.mapping.fromJS(data, mapping);
ko.applyBindings(observable, $root[0]);
JSFiddle
and it almost works.
I am successful with row creation - which was the siple part, but for the life of me I can't make the first input in the created raw to be focused.
Any ideas (and I went through a ton of suggestions, none of which worked in the above setting)?
I might be missing the point, but why not just set your hasFocus to editing() instead of true?
<div id="root">
<table>
<tbody data-bind="foreach: events">
<tr>
<td>
<input type="text" data-type="name" data-bind="value: name, css: {hidden: !editing()}, hasFocus: editing()">
</td>
<td>
<input type="text" data-type="method" data-bind="value: age, css: {hidden: !editing()}">
</td>
</tr>
</tbody>
</table>
</div>
Fiddle here
It often pays of to avoid mixing jQuery with Knockout like that. For most cases Knockout also has options. In your case, the ones to look at are the event binding and the hasFocus binding.
Looking at your code, I'd update the View to something like this:
<tr>
<td>
<input type="text" data-bind="value: name,
hasFocus: editingName">
</td>
<td>
<input type="text" data-bind="value: age,
hasFocus: editingAge,
event: { blur: $root.onBlurAgeEdit }">
</td>
</tr>
As an aside, if you're after "MS Word Style" creation of new rows when a user presses tab, you can also bind to the "keypress" event and check for the correct keycode (for tab).
Notice that I've split the editing observable in two observables: editingName and editingAge.
The function onBlurAgeEdit will fire whenever the age input blurs. It looks like this:
self.onBlurAgeEdit = function (item) {
// Unwrap the array
var myEvents = self.events();
// Check if this function was called for the *last* item in the array
if (myEvents[myEvents.length-1] === item) {
// Make sure the last item isn't in edit mode anymore
item.editingAge(false);
// Push a new item that has *name* in edit mode
self.events.push({name: ko.observable(''),
age: ko.observable(''),
editingName: ko.observable(true),
editingAge: ko.observable(false)});
}
};
Note that for Firefox you'll need to throttle the observables to make the hasFocus binding play nice.
See all this at work in this fiddle.

Categories

Resources