my first post and my first polymer app so I appreciate your patience. I'm using paper-tabs and iron-pages to enclose two vaadin-grids that populate via the same iron-ajax set-up - slightly different parameters. The code I have is as follows:
<ajax-api
data="{{apidata}}"
sortby="{{sortColumn}}"
gender="{{gen}}"
cat="{{category}}"
weapon="{{weap}}">
</ajax-api>
<paper-tabs selected="{{selected}}" sticky>
<paper-tab>Mens {{category}} {{weap}}</paper-tab>
<paper-tab>Womens {{category}} {{weap}}</paper-tab>
</paper-tabs>
<iron-pages selected="{{selected}}">
<div>
<vaadin-grid id="mens" items="[[apidata.mens]]" visible-rows = 15 >
<table>
<colgroup>
<col name="rank" />
.
.
.
<col name="cat" hidable="" hidden="" />
</colgroup>
</table>
</vaadin-grid>
</div>
<div>
<vaadin-grid id="womens" items="[[apidata.womens]]" visible-rows = 15>
<table>
<colgroup>
<col name="rank" />
.
.
.
<col name="cat" hidable="" hidden="" />
</colgroup>
</table>
</vaadin-grid>
</div>
Currently as you can probably see I'm using one ajax call to populate both mens and womens data at the same time - doesnt seem too efficient and is a pain when I start sorting it. The element ajax-api has two way binding that forms the parameters of an iron-ajax call and returns the data. I'd like to have the data load separately on selection of the tab. To complicate matters I'm also using the grid column renderer to re-style some of the data. I'm figuring having selection change the bound gender property will trigger my ajax element so my question is how do I do that and how can I then ensure my grid column renderer function is referencing the correct grid.
EDIT:
I thought maybe I should add the content of the Polymer function to see if that helps someone frame a response.
<script>
Polymer({
is: 'my-rankings1',
properties: {
sortColumn: {
type: String,
value: "points",
},
sortDirection: String,
sortProperty: String
},
ready: function() {
var grid = this.$.mens;
this.selected = 0;
grid.addEventListener('sort-order-changed', function() {
if (grid.size > 0) {
grid.scrollToStart();
var sortOrder = grid.sortOrder[0];
this.sortColumn = grid.columns[sortOrder.column].name;
alert(this.sortColumn);
grid.refreshItems();
}
}.bind(this));
grid.then(function() {
// Add a renderer for the index column
grid.columns[0].renderer = function(cell) {
cell.element.innerHTML = cell.row.index +1;
};
grid.columns[6].renderer = function(cell) {
if (cell.data < 0){
cell.element.innerHTML = Math.abs(cell.data) + '<iron-icon icon="arrow-downward" style="color: red"/>';
}
else if (cell.data > 0) {
cell.element.innerHTML = cell.data + '<iron-icon icon="arrow-upward" style="color: green"/>';
}
else {cell.element.innerHTML = '<iron-icon icon="settings-ethernet" style="color: #ffcc00"/>';}
};
});
},
});
Related
I'm trying to create a table where the user can hide rows, and keep them hidden as they delete other rows.
In my html I use vuejs to bind a class when rendering the table:
<tr v-for="item in mylist" :class="{'noFruits': (item.fruits.length == 0)}">
There is a user checkbox to hide rows with that class:
<label><input type="checkbox" v-model="showBlankFruits" #change="setBlankDisplay">Show Blank Fruits</label>
In my Vue instance, the checkbox executes a method to hide/show rows with that class via jquery to attach the css display property:
methods: {
setBlankDisplay: function() {
if (this.showBlankFruits) {
$('.noFruits').css('display', '');
} else {
$('.noFruits').css('display', 'none');
}
},
In my jsfiddle, when a user deletes a row, the hidden row reappears. I see that attaching styles with jquery in this instance is not good... does anyone have a suggestion for a better method?
Mixing Vue and jQuery is not recommended, as you can do pretty much everything just using Vue and you don't get any conflicting operations that don't know what the other library/framework is doing.
The following will show the row if either the fruits array length is truthy, in other words not 0, or if showBlankFruits is true:
<tr v-for="item in mylist" v-show="item.fruits.length || showBlankFruits">
The following will toggle showBlankFruits when clicking the checkbox:
<label><input type="checkbox" v-model="showBlankFruits">Show Blank Fruits</label>
Full code example:
JSFiddle
You can also write something like this.
I've used computed and removed the jQuery part completely.
You must declare data as a function instead of an data object (https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function)
You do not need to call the mounted method to set the initial state. It's already set with your data object.
In your code, you have to call mounted, because jQuery can only hide the results, when the DOM is loaded.
new Vue({
el: '#app',
data() {
return {
showBlankFruits: true,
mylist: [
{'user': 'Alice', 'fruits': [{'name': 'apple'}]},
{'user': 'Bob', 'fruits': [{'name': 'orange'}]},
{'user': 'Charlie', 'fruits': []},
{'user': 'Denise', 'fruits': [{'name': 'apple'}, {'name': 'orange'}]},
]
}
},
computed: {
list() {
return this.mylist.filter(item => (item.fruits.length > 0 && !this.showBlankFruits) || (item.fruits.length === 0 && this.showBlankFruits))
},
},
methods: {
delItem(item) {
let index = this.mylist.indexOf(item);
if (index > -1) {
this.mylist.splice(index, 1);
}
}
}
})
<script src="https://unpkg.com/vue"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<div id="app">
<label><input type="checkbox" v-model="showBlankFruits">Show Blank Fruits</label>
<br> <br>
<table>
<tr> <th class="col">User</th> <th class="col">Fruits</th> <th class="col"></th> </tr>
<tr v-for="item in list">
<td>{{ item.user }}</td>
<td> <span v-for="f in item.fruits"> {{ f.name }} </span> </td>
<td> <button #click="delItem(item)">Delete</button> </td>
</tr>
</table>
</div>
This is a great example of the power for declarative rendering rather than using DOM manipulation in components. The problem you are running into is when the Vue engine re-renders your list it doesn't know about the manual manipulation of elemets you did in the setBlankDisplay method. The way to get around that is to use component logic in the definition of the view itself, sort of like you did to set the noFruits class in the first place.
So, I propose you get rid of setBlankDisplay and replace it with the method:
itemDisplay(item) {
if (item.fruits.length === 0 && !this.showBlankFruits) {
return 'none';
}
return '';
},
Then you can reference it in the definition of your tr elements linked to a css display property, like so:
<tr v-for="item in mylist" :class="{'noFruits': (item.fruits.length == 0)}" :style="{display: itemDisplay(item)}">
I've updated the jsfiddle with this modification, showing that the state of hidden fruits remains when other items are deleted.
Take this as a general example of the dangers of using jquery to change the state of the view. Every effort should be taken to define the entire view in terms of component logic.
I am using jsrender to write a bootstrap based HTML template and render the HTML content using javascript. My data is as follows
var data = [
{
'img': 'img/books/kiterunner.jpg',
'title': 'The Kite Runner',
'authors': 'Khaled Hosseini'
},
{
'img': 'img/books/tokillamockingbird.jpg',
'title': 'To Kill A Mocking Bird',
'authors': 'Harper Lee'
},
{
'img': 'img/books/hungergames.jpg',
'title': 'The Hunger Games',
'authors': 'Suzanne Collins'
}
.....
]
And the corresponding HTML I want to generate is as follows
<div class="row-fluid" id="result">
<script id="template" type="text/x-js-render">
<div class="col-md-3 col-sm-6">
<div class="image-tile outer-title text-center">
<img alt="{{:title}}" src="{{:img}}" style="max-height:350px;">
<div class="title mb16">
<h5 class="uppercase mb0">{{:title}}</h5>
<span>{{:authors}}</span>
</div>
</div>
</div>
</script>
</div>
Currently each object in data gets displayed individually but sometimes, the images are larger in size and disrupt the way the row looks. So I want to repeat the div.row-fluid creation with the content after every 4 objects from data. I currently do the following to register the template and render the data into the required HTML. I can't seem to figure out how to use {{for}} without changing the values in data.
var template = $.templates("#template");
var htmlOutput = template.render(data);
$("#result").html(htmlOutput);
Thank you.
Edit: I have managed to do this by doing explicit HTML string concatenation and then rendering them while using index%4 in the if statements to decide on whether the div.row-fluid needs to be created. However, I would really like a more elegant solution. Adding jquery tag for wider visibility.
You could use a custom {{range}} tag - see this sample.
Here is an example which renders into a table with <td>s grouped by four - for a four column layout:
<script id="myTmpl" type="text/x-jsrender">
<table>
{{for rows ~last=rows.length-1}}
{{if #index%4===0 ~ind=#index}}
<tr>
{{range ~root.rows start=~ind end=~ind+3 max=~last}}
<td>{{:title}}</td>
{{/range}}
</tr>
{{/if}}
{{/for}}
</table>
</script>
using this version of a custom {{range}} tag:
$.views.tags("range", function(array) {
var ret = "",
start = this.tagCtx.props.start,
end = this.tagCtx.props.end,
max = this.tagCtx.props.max;
end = end > max ? max : end;
for (var i = start; i <= end; i++) {
// Render tag content, for this data item
ret += this.tagCtx.render(array[i]);
}
return ret;
});
var data = {rows: [
{
'title': ...
},
...
]};
...
I am very new to vuejs, and I am working with making a dynamic table where the left column will have a hidden input that will ultimately query a database and display the result in a pop up (for a quick cross referencing ability). I have made it to the point where it builds the table properly with a v-for, but I can't quite figure out how to bind the dynamic v-models to the js function that will run the process. If I am going about this the completely wrong way, please let me know. Cheers and thanks!
...
<tr v-for="tableRow in rtnUnsubs">
<td class="unsubCell">
<input name= "[tableRow.share_id]" v-model="[tableRow.share_id]" value="[tableRow.share_id]">{{ tableRow.share_id }}
<button v-on:click="getSub">view</button>
</td>
<td class="unsubCell">{{ tableRow.unsubscriber_type }}</td>
<td class="unsubCell">{{ tableRow.unsubscriber_id }}</td>
</tr>
...
<script>
...
getSub(/*v-model from input*/) {
window.alert(/*do some stuff with v-model*/)
return;
}
Ordinarily, the method bound to a click will get the event, but you can pass whatever you want it to get (include $event if you want to add that to other arguments).
When you say you want to "bind v-models", I take it to mean you want to pass the current data.
new Vue({
el: '#app',
data: {
rows: [{
share_id: 'IT ME'
},
{
share_id: 'THE OTHER ONE'
}
]
},
methods: {
getSub(data) {
console.log("Working with", data);
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<div v-for="tableRow in rows">
<input name="tableRow.share_id" v-model="tableRow.share_id" value="tableRow.share_id">{{ tableRow.share_id }}
<button v-on:click="getSub(tableRow.share_id)">view</button>
</div>
</div>
I have an table element where the declaration is as follows
<euro-table id="euroTable" number-visible-rows="10">
<euro-column title="Id" type="text" key="Id"></euro-column>
<euro-column title="Descripcion" type="text" key="Descripcion"></euro-column>
<euro-column title="Abreviatura" type="text" key="ShortName"></euro-column>
<euro-column title="Tipo" type="object" key="FeeType" objectkey="Descripcion"></euro-column>
<euro-column title="Monto($)" type="text" key="Monto"></euro-column>
<euro-column title="Cobrar a" type="array" key="NivelesEscolares" objectkey="Descripcion"></euro-column>
</euro-table>
Data is added using javascript after an iron-ajax request. Everything works as it should work, except for one thing: when I use dom-repeat to bind added data I use <dom-if> template because depending on the type of column, I must access and display the corresponding information. The code I use to do that is the following:
<template is="dom-repeat" items="{{visibleRows}}" id="tableRow" as="row">
<tr on-tap="rowSelected" class$="{{getClass(item.active)}}">
<template is="dom-repeat" items="{{headers}}" id="tableRow2" as="column">
<template is="dom-if" if="{{getType(column.type, 'object')}}">
<td>
<li>{{getObjectValue(column,row)}}</li>
</td>
</template>
<template is="dom-if" if="{{getType(column.type, 'array')}}">
<td>
<template is="dom-repeat" items="{{getDataArray(column,row)}}">
<li>{{getObjectValue(column,row)}}</li>
</template>
</td>
</template>
<template is="dom-if" if="{{getType(column.type, 'text')}}">
<td>{{getValue(column,row)}}</td>
</template>
</template>
</tr>
</template>
So my problem is that I can not display the information correctly and I think the reason is for the use of dom-repeat. My information is displayed as follows:
The information is out of the table, I'm a checking my getType function but I think its ok. Any idea about to fix my bug? Thanks!
After I had researched a little more, I found here that my problem was a bug of polymer. To solve it, is necessary to change two functions in Polymer.html file. The functions are _wrapTextNodes and _showHideChildren. I will leave the functions here just in case anyone have the same problem.
_wrapTextNodes: function(root) {
// wrap text nodes in span so they can be hidden.
for (var n = root.firstChild; n; n=n.nextSibling) {
if (n.nodeType === Node.TEXT_NODE && n.textContent.trim.length) {
var s = document.createElement('span');
root.insertBefore(s, n);
s.appendChild(n);
n = s;
}
}
},
_showHideChildren: function() {
var hidden = this._hideTemplateChildren || !this.if;
if (this._instance) {
var c$ = this._instance._children;
for (var i=0; i<c$.length; i++) {
var c = c$[i];
if (c.nodeType !== Node.TEXT_NODE) {
c.style.display = hidden ? 'none' : '';
c._hideTemplateChildren = hidden;
}
}
}
},
Here you can find dom-if.html file.
I am new to Knockout and I am building a Simple POC for using knockout to build SPA(Single Page Application).
What I want to do is to show "Business Units" when the app loads and on selection of a business unit show all "Front End Units" under that business unit and on selection of a front end unit, show all "Sales Segments" under that front end unit.
All this will happen in a single page using the same view and the viewmodel will bind the model based on selected business unit or front end unit.
The issue I am facing is that, I have 5 business units that get bound properly first on document ready, but on selection of business unit, the front end units get repeated 5 times each. In this case, I have 2 front end units and each is shown 5 times. Same issue on selection of front end unit.
You can see this issue mimicked in the following jsFiddle sample - jsFiddle Link
Let me know if you can't access the jsfiddle link. In this sample, I have used arrays, but in actual I will be getting the data through async call to the oData service.
This is the view HTML:
<div id="divbu">
<h4 data-bind="text: Heading"></h4>
<ul data-role="listview" data-inset="true" data-bind="foreach: Collection">
<li data-role="list-divider" data-bind="text: EntityName"></li>
<li>
<a href="#" data-bind="click: $root.fnNextLevel">
<table border="0">
<tr>
<td>
<label style="font-size: 12px;">Bus. Plan: </label>
</td>
<td>
<label style="font-size: 12px;" data-bind="text: BusinessPlan"></label>
</td>
<td>
<label style="font-size: 12px;">Forecast: </label>
</td>
<td>
<label style="font-size: 12px;" data-bind="text: Forecast"></label>
</td>
</tr>
<tr>
<td>
<label style="font-size: 12px;">Gross Sales: </label>
</td>
<td colspan="3">
<label style="font-size: 12px;" data-bind="text: GrossSales"></label>
</td>
</tr>
</table>
</a>
</li>
</ul>
</div>
This is the model and view model:
function CommonModel(model, viewType) {
var self = this;
if (viewType == 'BU') {
self.EntityName = model[0];
self.BusinessUnit = model[0];
self.BusinessPlan = model[1];
self.Forecast = model[2];
self.GrossSales = model[3];
} else if (viewType == 'FEU') {
self.EntityName = model[1];
self.BusinessUnit = model[0];
self.FrontEndUnit = model[1];
self.BusinessPlan = model[2];
self.Forecast = model[3];
self.GrossSales = model[4];
} else if (viewType == 'SS') {
self.EntityName = model[2];
self.BusinessPlan = model[3];
self.Forecast = model[4];
self.GrossSales = model[5];
}
}
function ShipmentReportsViewModel(results, viewType) {
var self = this;
self.Collection = ko.observableArray([]);
for (var i = 0; i < results.length; i++) {
self.Collection.push(new CommonModel(results[i], viewType));
}
if (viewType == 'BU') {
self.Heading = "Business Units";
self.fnNextLevel = function (businessUnit) {
FetchFrontEndUnits(businessUnit);
};
self.Home = function () {
FetchBusinessUnits();
};
} else if (viewType == 'FEU') {
self.Heading = results[0][0];
self.fnNextLevel = function (frontEndUnit) {
FetchSalesSegments(frontEndUnit);
};
self.Home = function () {
FetchBusinessUnits();
};
} else if (viewType == 'SS') {
self.fnNextLevel = function () {
alert('No activity zone');
};
self.Heading = results[0][0] + ' - ' + results[0][1];
self.Home = function () {
FetchBusinessUnits();
};
}
}
You can see the complete code in the jsFiddle link.
I have also tried this with multiple views and multiple view models, where I apply bindings by giving the element ID. In this case, one flow from business unit -> sales segment is fine, but when I click on home or back button and I do binding again to that element, I face the same issue. (home and back button features are not done in jsFiddle example).
Let me know if more details are required. I did look into lot of other links in stack overflow, but nothing addressing this particular problem.
Any help is deeply appreciated. Thanks in advance.
The problem here is that you call your ko.applybindings TWICE and there is a foreach binding that iterate within 5 items, therefore the data are duplicated five times.
you should not call a ko.applybindings more than once on the same model.
Your model is always the same even if it's parametrized.
I had the same problem here: Data coming from an ObservableArray are displayed twice in my table
the fact that you have you business logic inside your viewModel is something that could be discussed, and it makes it not easy to fix this.
Make 3 classes, put them in a common model without logic inside. Then once you have applyed the ko.applyBindings once, you just have to modify the array like this:
viewModel.myArray(newValues)
Here is the fiddle with the amended code: http://jsfiddle.net/MaurizioPiccini/5B9Fd/17/
it does not do exaclty what you need but if remove the multiple bindings by moving the Collection object scope outside of your model.
As you can see the problem IS that you are calling the ko.applybindings twice on the same model.
Finally, I got this working. Thanks to #MaurizioIndenmark.
Though I have removed multiple call for ko.applybindings, I was still calling the view model multiple times. This was causing the issue.
Now, I have cleaner view model and I have different function calls for different actions and modify all the data required to be modified within these functions(events). Now, everything is working as expected.
This is how the view model looks now -
function ShipmentReportsViewModel(results) {
var self = this;
self.Heading = ko.observable();
self.BusinessUnits = ko.observableArray();
self.FrontEndUnits = ko.observableArray();
self.SalesSegments = ko.observableArray();
self.Home = function () {
var bu = FetchBusinessUnits();
self.Heading("Business Units");
self.BusinessUnits(bu);
self.FrontEndUnits(null);
self.SalesSegments(null);
};
self.fnFeu = function (businessUnit) {
var feu = FetchFrontEndUnits(businessUnit);
self.Heading(feu[0].BusinessUnit);
self.FrontEndUnits(feu);
self.BusinessUnits(null);
self.SalesSegments(null);
};
self.fnSalesSeg = function (frontEndUnit) {
var ss = FetchSalesSegments(frontEndUnit);
self.Heading(ss[0].BusinessUnit + ' - ' + ss[0].FrontEndUnit);
self.SalesSegments(ss);
self.BusinessUnits(null);
self.FrontEndUnits(null);
};
self.Home();
}
To see the entire working solution, please refer this jsFiddle
Thanks for all the valuable suggestions in getting this work.