Conditional display with Dojo MVC - model-view-controller

I have a dojox/mvc/Repeat area which is bound to an array of records.
Within the row of the Repeat there is a field (the id of the record) which should be a simple display Output if the record has already been saved to the database, but it should be a TextBox if the record is new (the user must enter the value).
How do I solve this elegantly? I am fairly new to Dojo and its MVC part is very under-documented.

The most MVC-ish solution I have found so far is as follows:
1)
I put a "hasBeenSaved" property into the model which will mark the server-side saved state of the record. This attribute will be bound to the view with a transformation since the "display" style attribute of the DIV will be bound to the hasBeenSaved model attribute (one is a boolean the other is a string: "block"/"none").
2)
Within the Row, I put a conditionally visible div around the id input field. This will be visible only when the record is new, so its display style attribute is bound with an appropriate transformer attached to the Dojo MVC binding.
The same is done for the id output field but the transformer is different on the binding since this will be displayed only when the record has already been saved.
The JSFiddle which I have used to prototype this solution: http://jsfiddle.net/asoltesz/6t4dj1w7/15/
require([
"dojo/_base/declare", "dojo/dom-style", "dojo/parser", "dojo/ready",
"dijit/_WidgetBase", "dijit/_TemplatedMixin",
'dojox/mvc/getStateful'
], function(
declare, domStyle, parser, ready,
_WidgetBase, _TemplatedMixin,
getStateful
){
// setting up the data model for MVC
model = {
items: [
{ id: 'id1',
hasBeenSaved: true
},
{ id: null,
hasBeenSaved: false
},
{ id: null,
hasBeenSaved: false
},
{ id: 'id3',
hasBeenSaved: true
}
]
};
model = getStateful(model);
/**
* This mixin makes it possible to set the "display" style property of
* the DOM node (of any widget) as a Widget property and thus bind it to an MVC model
* when needed.
*/
declare("_DisplayAttributeMixin", [], {
// parameters
display: "block",
_setDisplayAttr: function(/*String*/ display){
this._set("display", display);
domStyle.set(this.domNode, "display", display);
}
});
/** Transformer methods for converting hasBeenSaved to visible/hidden values */
transfSavedToHidden = {format: function(hasBeenSaved){
console.log("transfSavedToHidden: " + (hasBeenSaved ? "none" : "block"));
return hasBeenSaved ? "none" : "block";
}};
transfSavedToVisible = {format: function(hasBeenSaved){
console.log("transfSavedToHidden: " + (hasBeenSaved ? "block" : "none"));
return hasBeenSaved ? "block" : "none";
}};
ready(function(){
// Call the parser manually so it runs after our mixin is defined, and page has finished loading
parser.parse();
});
});
The HTML markup:
<script type="dojo/require">at: "dojox/mvc/at"</script>
<div
data-dojo-type="dojox/mvc/Group"
data-dojo-props="target: model"
>
<div id="repeatId"
data-dojo-type="dojox/mvc/Repeat"
data-dojo-props="children: at('rel:', 'items')"
>
<div
data-dojo-type="dojox/mvc/Group"
data-dojo-props="target: at('rel:', ${this.index})"
>
<span>Record: ${this.index}</span>
<!-- This is displayed only when the record is new (not saved yet) -->
<div
data-dojo-type="dijit/_WidgetBase"
data-dojo-mixins="_DisplayAttributeMixin"
data-mvc-bindings="
display: at('rel:', 'hasBeenSaved')
.direction(at.from)
.transform(transfSavedToHidden)"
>
<label for="idInput${this.index}">id:</label>
<input
data-dojo-type="dijit/form/TextBox"
id="idInput${this.index}"
data-dojo-props="value: at('rel:', 'id')"
></input>
</div> <!-- end conditionally hidden div -->
<!-- This is displayed only when the record has already been saved -->
<div
data-dojo-type="dijit/_WidgetBase"
data-dojo-mixins="_DisplayAttributeMixin"
data-mvc-bindings="
display: at('rel:', 'hasBeenSaved')
.direction(at.from)
.transform(transfSavedToVisible)"
>
<label for="idInput${this.index}">id:</label>
<span
data-dojo-type="dojox/mvc/Output"
id="idOutput${this.index}"
data-dojo-props="value: at('rel:', 'id')"
></span>
</div> <!-- end conditionally hidden div -->
<hr/>
</div> <!-- end of row -->
</div> <!-- end of Repeat -->
</div> <!-- end of Group -->

A secondary, less complex solution:
Bind the "hasBeenSaved" property to a hidden text within the repeating div.
Put an onChange event on the hidden field which gets the index of the repeat as well.
The onChange event simply hides the field which is not appropriate in light of the hasBeenChanged property value for the record.
The fiddle is here: http://jsfiddle.net/asoltesz/8u9js6sz/5/
Code:
hasBeenSavedChanged = function(field, index) {
var divToHide
if (field.value == true) {
divToHide = "idInputDiv"
}
else {
divToHide = "idOutputDiv"
}
var div = document.getElementById(divToHide + index);
div.style.display = "none";
}
require([
"dojo/_base/declare", "dojo/dom-style", "dojo/parser", "dojo/ready",
"dijit/_WidgetBase", "dijit/_TemplatedMixin",
'dojox/mvc/getStateful'
], function(
declare, domStyle, parser, ready,
_WidgetBase, _TemplatedMixin,
getStateful
){
// setting up the data model for MVC
model = {
items: [
{ id: 'id1',
hasBeenSaved: true
},
{ id: null,
hasBeenSaved: false
},
{ id: null,
hasBeenSaved: false
},
{ id: 'id3',
hasBeenSaved: true
}
]
};
model = getStateful(model);
ready(function(){
// Call the parser manually so it runs after our mixin is defined, and page has finished loading
parser.parse();
});
});
HTML:
<script type="dojo/require">at: "dojox/mvc/at"</script>
<div
data-dojo-type="dojox/mvc/Group"
data-dojo-props="target: model"
>
<span id="itemsCtl"
data-dojo-type="dojox/mvc/ListController"
data-dojo-props="model: model.items">
</span>
<div id="itemsRepeat"
data-dojo-type="dojox/mvc/Repeat"
data-dojo-props="children: at('rel:', 'items')"
>
<div
data-dojo-type="dojox/mvc/Group"
data-dojo-props="target: at('rel:', ${this.index})"
>
<span>Record: ${this.index}</span>
<input
id="hasBeenChanged${this.index}"
data-dojo-type="dijit/form/TextBox"
data-dojo-props="value: at('rel:', 'hasBeenSaved')"
onChange="hasBeenSavedChanged(this, '${this.index}');"
type="hidden"
></input>
<!-- This is displayed only when the record is new (not saved yet) -->
<div id="idInputDiv${this.index}"
>
<label for="idInput${this.index}">id:</label>
<input
data-dojo-type="dijit/form/TextBox"
id="idInput${this.index}"
data-dojo-props="value: at('rel:', 'id')"
></input>
</div> <!-- end conditionally hidden div -->
<!-- This is displayed only when the record has already been saved -->
<div id="idOutputDiv${this.index}" >
<label for="idInput${this.index}">id:</label>
<span
data-dojo-type="dojox/mvc/Output"
id="idOutput${this.index}"
data-dojo-props="value: at('rel:', 'id')"
></span>
</div> <!-- end conditionally hidden div -->
<hr/>
</div> <!-- end of row -->
</div> <!-- end of Repeat -->
</div> <!-- end of Group -->

Mixin/transformer approach is something I had in mind, too. Two things I’d add there are out-of-the-box Dijit features, one is dijitDisplayNone class, another is attribute mapping feature.
Though it’s strange that the former is undocumented, the intent may have been for private usage within Dijit codebase.
Though it’s a bit hackish (and may be broken in future 1.x Dijit releases), overriding the Dijit code that’s responsible for attribute mapping will allow you to map a widget attribute to a CSS class that’s toggled.
Here’s a code sample that uses the above two:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="//ajax.googleapis.com/ajax/libs/dojo/1.10.1/dojo/resources/dojo.css">
<link rel="stylesheet" type="text/css" href="//ajax.googleapis.com/ajax/libs/dojo/1.10.1/dijit/themes/dijit.css">
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/dojo/1.10.1/dojo/dojo.js" data-dojo-config="async: 1, parseOnLoad: 0"></script>
<script type="text/javascript">
require([
"dojo/_base/array",
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/dom-class",
"dojo/parser",
"dojox/mvc/getStateful",
"dijit/form/TextBox"
], function(array, declare, lang, domClass, parser, getStateful){
declare("CssToggleMixin", null, {
// summary:
// Mixin class to support widget attributes with toggleClass type.
// toggleClass type allows boolean value of an attribute to reflect existence of a CSS class in a DOM node in the widget.
_attrToDom: function(/*String*/ attr, /*String*/ value, /*Object?*/ commands){
// summary:
// Handle widget attribute with toggleClass type.
// See dijit/_WidgetBase._attrToDom() for more details.
var callee = arguments.callee;
array.forEach((function(){ return lang.isArray(commands) ? commands.slice(0) : [commands]; })(arguments.length >= 3 ? commands : this.attributeMap[attr]), function(command){
command.type != "toggleClass" ?
this.inherited("_attrToDom", lang.mixin([attr, value, command], {callee: callee})) :
domClass.toggle(this[command.node || "domNode"], command.className || attr, value);
}, this);
}
});
flipConverter = {
format: function (value) {
return !value;
},
parse: function (value) {
return !value;
}
};
model = getStateful({
items: [
{
value: "Foo",
hasBeenSaved: true
},
{
hasBeenSaved: false
},
{
hasBeenSaved: false
},
{
value: "Bar",
hasBeenSaved: true
}
]
});
parser.parse();
})
</script>
</head>
<body>
<script type="dojo/require">at: "dojox/mvc/at"</script>
<div data-dojo-type="dojox/mvc/WidgetList"
data-dojo-mixins="dojox/mvc/_InlineTemplateMixin"
data-dojo-props="children: at(model, 'items')">
<script type="dojox/mvc/InlineTemplate">
<div>
<span data-dojo-type="dijit/_WidgetBase"
data-dojo-mixins="CssToggleMixin"
data-dojo-props="value: at('rel:', 'value'),
noDisplay: at('rel:', 'hasBeenSaved').transform(flipConverter),
_setValueAttr: {node: 'domNode', type: 'innerText'},
_setNoDisplayAttr: {type: 'toggleClass', className: 'dijitDisplayNone'}"></span>
<span data-dojo-type="dijit/form/TextBox"
data-dojo-mixins="CssToggleMixin"
data-dojo-props="value: at('rel:', 'value'),
noDisplay: at('rel:', 'hasBeenSaved'),
_setNoDisplayAttr: {type: 'toggleClass', className: 'dijitDisplayNone'}"></span>
</div>
</script>
</div>
</body>
</html>
Hope it’ll shed some light.
Best, Akira

Related

Knockout observable is not accessible inside function

So I am working with Knockout inside Magento 2.3.4, and I am setting a custom observable value on initialize, and then I am trying to access that observable and change the value inside a function. Every time I try, I keep getting "it is not a function", and it won't let me either retrieve and read the current observable value, or set a new one. When I try to run .isObservable() on it it comes up as false. I have looked through various examples of how to do it and tried all of them and none of them work. Currently my knockout JS form looks like this:
define([
'jquery',
'uiComponent',
'ko'
], function($, Component, ko) {
'use strict';
return Component.extend({
defaults: {
template: 'Shmoop_Cms/career-form'
},
progressText: ko.observable(false),
initialize: function() {
var self = this;
this._super();
this.progressText('1 of 15 questions completed');
return this;
},
showNext: function() {
let dataIndex = parseInt($('.quiz-card.show').attr('data-index')) + 1;
alert(ko.isObservable(this.progressText));
alert(this.progressText());
this.progressText(dataIndex + ' of 15 questions completed');
}
});
});
I am able to set the progressText value initially inside that initialize function without issue, and it recognizes there that it is an observable. Why does it say it's not an observable inside by "showNext" function?
FYI I have also tried adding "var self = this" inside my function, I have also tried "self.progressText()" instead of "this.progressText()", nothing worked.
Please help.
Edited: By the way, my template looks like this:
<div class="career-quiz-wrapper">
<!-- ko if: quizQuestions -->
<form class="career-quiz-form" data-bind="foreach: quizQuestions">
<div class="quiz-card" data-bind="attr: {'data-question-uuid': uuid, 'data-index': index }, css: index == 0 ? 'show' : 'hide'">
<h2 class="display-heading title">How important is it to you to...</h2>
<div class="lead main-text" data-bind="html: question"></div>
<div class="choices">
<div class="row-bar">
<input type="range" data-anchor="range" class="custom-range" min="0" max="100" value="50" autocomplete="off" data-bind="event: { change: $parent.adjustRangeSlider }">
</div>
<div class="row-options">
<div class="option-section" data-bind="foreach: choiceUUIDs">
<div class="choice-option-button">
<a class="option-button" data-bind="click: $parents[1].choiceClicked, attr: { 'data-choice-uuid': UUID, 'data-range-value': rangeValue, title: textOption }, text: textOption, css: (i && i == 2) ? 'selected' : ''"></a>
</div>
</div>
<div class="mobile-bar">
<input type="range" data-anchor="range" class="custom-range" min="0" max="100" value="50" autocomplete="off" data-bind="event: { change: $parent.adjustRangeSlider }">
</div>
</div>
</div>
<div class="choice-buttons">
<div class="choice-button">
<a data-bind="click: $parent.showCareers" class="blue-link" title="Show Careers">Show Careers</a>
</div>
<!-- ko if: index == ($parent.quizQuestions().length - 1) -->
<div class="choice-button">
<a class="pink-button results-button" data-bind="click: $parent.getResults" title="Get Results">Get Results <i class="fa fa-cog fa-spin d-none"></i></a>
</div>
<!-- /ko -->
<!-- ko ifnot: index == ($parent.quizQuestions().length - 1) -->
<div class="choice-button">
<a class="pink-button next-question" data-bind="click: $parent.showNext" title="Next Question">Next Question</a>
</div>
<!-- /ko -->
</div>
<div class="quiz-progress">
<p class="progress-text" data-bind="text: $parent.progressText"></p>
</div>
</div>
</form>
<!-- /ko -->
<!-- ko ifnot: quizQuestions -->
<div class="error-message">
<p>
Sorry, something went wrong. I guess you'll have to figure out what to do on your own..
</p>
</div>
<!-- /ko -->
I have had issues with javascript object literals before and would probably approach it like this. Not sure if this works as I haven't tested it.
define([
'jquery',
'uiComponent',
'ko'
], function($, Component, ko) {
'use strict';
var self = this;
self.progressText = ko.observable(false);
function initialize() {
this._super();
self.progressText('1 of 15 questions completed');
return this;
}
function showNext() {
let dataIndex = parseInt($('.quiz-card.show').attr('data-index')) + 1;
alert(ko.isObservable(self.progressText));
alert(self.progressText());
self.progressText(dataIndex + ' of 15 questions completed');
}
return Component.extend({
defaults: {
template: 'Shmoop_Cms/career-form'
},
progressText: self.progressText,
initialize: initialize,
showNext: showNext
});
});

Grails, sorting and filtering a grid at the same time using ajax

I have a grid(html-table) with a lot of columns and want to combine filtering and sorting on this table.
At the moment I only use filtering on one column but sorting on several columns.
I want to do this with ajax.
I read an article [http://www.craigburke.com/2011/01/23/grails-ajax-list-with-paging-sorting-and-filtering.html]
and tried to adapt it to my version of grails-3.2.6.
This has been very hard to solve and now I'm totally stuck.
If I add something in the filter nothing happens but when I click on a column, the filtering takes place and also sorting, clicking a second time, ajax is not called and the filter is overwritten with the default value. I have managed to implement it on a test project which act the same.
It is much code and maybe there is a way to include the whole project in this question in some way?
I'll try to show most of the important part here if it could help.
The index.gsp:
<!DOCTYPE html>
<html>
<head>
<meta name="layout" content="main" />
<g:set var="entityName" value="${message(code: 'person.label', default: 'Person')}" />
<title><g:message code="default.list.label" args="[entityName]" /></title>
<script type="text/javascript">
$(document).ready(function() {
setupGridAjax();
setupFilterAjax();
});
</script>
<script type="text/javascript">
function setupGridAjax() {
$('#gridPersons').find('.paginateButtons a, th.sortable a').on('click', function(event) {
event.preventDefault();
var url = $(this).attr('href');
var grid = $(this).parents("table.ajax");
$(grid).html($("#spinner").html());
$.ajax({
type: 'GET',
url: url,
data: [tag],
success: function(data) {
$(grid).fadeOut('fast', function() {$(this).html(data).fadeIn('slow');});
}
})
});
}
</script>
<script type="text/javascript">
// Turn any input changes or form submission within a filter div into an ajax call
function setupFilterAjax(){
alert('FILTER--Anropat');
$('div.filters select:input').on('change',function(event) {
var filterBox = $(this).parents("div.filters");
filterGrid(filterBox);
});
$("div.filters form").submit(function() {
var filterBox = $(this).parents("div.filters");
alert('FILTERBOX - '+filterBox);
filterGrid(filterBox);
return false;
});
}
// Reload grid based on selections from the filter
function filterGrid(filterBox) {
alert('FILTER-change detected');
var grid = $(filterBox).next("div.gridPersons");
$(grid).html($("#spinner").html());
var form = $(filterBox).find("form");
var url = $(form).attr("action");
var data = $(form).serialize();
alert('FILTERGRID - '+url);
$.ajax({
type: 'POST',
url: '${g.createLink( controller:'person', action:'index' )}',
data: [tag],
success: function(data) {
$(grid).fadeOut('fast', function() {$(this).html(data).fadeIn('slow');});
}
});
}
</script>
</head>
<body>
<g:message code="default.link.skip.label" default="Skip to content…"/>
<div class="nav" role="navigation">
<ul>
<li><a class="home" href="${createLink(uri: '/')}"><g:message code="default.home.label"/></a></li>
<li><g:link class="create" action="create"><g:message code="default.new.label" args="[entityName]" /></g:link></li>
</ul>
</div>
<div id="list-person" class="content scaffold-list" role="main">
<h1><g:message code="default.list.label" args="[entityName]" /></h1>
<g:if test="${flash.message}">
<div class="message" role="status">${flash.message}</div>
</g:if>
<div class="filters">
<g:form action="register">
<div id="selectMill">
Select tags:
<g:select class="selected" name="tag" from="${tagList}" value="${filters?.tag}" noSelection = "${['':'All']}" optionValue="" optionKey="" />
</div>
<div id="gridPersons">
<g:render template="Grid_Persons" model="personList" />
</div>
<div class="pagination">
<g:paginate total="${personCount ?: 0}" />
</div>
<fieldset class="buttons">
<input class="save" type="submit" value="${message(code: 'offer.create.from.buffer.label', default: 'Create')}" />
</fieldset>
</g:form>
</div>
</div>
</body>
The template: _Grid_Persons.gsp
<table class="ajax">
<thead>
<tr>
<g:sortableColumn property='reg' title='Register' />
<g:sortableColumn property="id" title='Id' params="${filters}"/>
<g:sortableColumn property='name' title='Name' params="${filters}"/>
<g:sortableColumn property='tag' title='Tag' params="${filters}"/>
<g:sortableColumn property='registered' title='Registered' params="${filters}"/>
</thead>
<tbody>
<g:each in="${personList}" status="i" var="ps">
<tr class="${ (i % 2) == 0 ? 'even': 'odd'}">
<td><g:checkBox name="ckb" value="${ps.id}" checked="false" /></td>
<td><g:link action="edit" id="${ps.id}">${ps.id}</g:link></td>
<td>${ps.name}</td>
<td>${ps.tag}</td>
<td>${ps.registered}</td>
<td>
</g:each>
</tbody>
The index-part of the controller:
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
def tagList = Person.withCriteria {
projections {
distinct("tag")
}
}
def List<Person> personList = getPersonList()
// Paging def prodBuffer = getPaginatedList(prodBuffer, max, params.offset?.toInteger())
def filters = [tag: params.tag, sort: params.sort, order: params.order]
def model = [personList: personList, filters:filters, tagList:tagList]
if (request.xhr) {
println("AJAX-Request!!!")
render(template:"Grid_Persons", model:model)
prodBuffer, offerDetails:offerDetails, filters:filters])
} else {
offerDetails: offerDetails, millList: millList, selectedMill:false, prodBufferCount: ProdBuffer.count()]
[personList:personList,tagList:tagList]
}
Person.count(), tagList:tagList]
}
def List<Person> getPersonList() {
println("getPersonList tag: "+params.tag)
def tag = params.tag
def c = Person.createCriteria()
def tempList = c.list {
if (tag) eq("tag", tag)
if (params.sort){
order(params.sort, params.order)
}
}
return tempList
}
From debugging:
The page is loaded:
getPersonList tag: null
Selected "Grails" in the filter:
getPersonList tag: Grails
AJAX-Request!!!
Klicked on the "Name"-column header
getPersonList tag: Grails
AJAX-Request!!!
-- Now the list is sorted(ascending) and filtered
Klicked on the column again:
getPersonList tag: Grails
-- Now the list is resorted(descending) and the filtering still ok but the SELECT TAGS now view "All"
Klicked the column for the third time:
getPersonList tag:
AJAX-Request!!!
-- Now the list show all lines and resorted(ascending)
Now solved by using the recommended plugin - datatables.
In case you are in a hurry you can inject https://datatables.net/ that adds the utilities you require among others

Prevent component from rendering input attributes

I have a my-tag component that simply renders a title:
html
<div id="content"></div>
<script id="main-template" type="text/mustache">
<my-tag title="This is the title"></my-tag>
</script>
javascript
var Component = can.Component.extend({
tag: 'my-tag',
template: '<h1>{{title}}</h1>',
viewModel: {
title: '#'
}
});
$('#content').html(can.view('main-template', {}));
output
<div id="content">
<my-tag title="This is the title">
<h1>This is the title</h1>
</my-tag>
</div>
I would like to have the output as follows:
<div id="content">
<my-tag>
<h1>This is the title</h1>
</my-tag>
</div>
How can I get the component to not render the title attribute in my-tag?
Here is the jsfiddle.
You can't prevent it from rendering, however, you might be able to remove it after the component is created like:
var Component = can.Component.extend({
tag: 'my-tag',
template: '<h1>{{title}}</h1>',
viewModel: {
title: '#'
},
events: {
init: function(){
this.element.removeAttr("title");
}
}
});
Also, if you are starting a new CanJS project, I'd encourage you to switch to can.stache as that will be the default templating engine in 3.0. It's highly compatible with can.mustache.

Knockout-kendo KendoUI Grid Sorting by the input value in the grid cell

I have a KendoUI grid that is defined in my code using the Knockout-Kendo plugin as follow. It is important to note that this grids number of columns changes depending on the json received from the server.
<script type="text/html" id="grid">
<div class="grid-input input" data-bind="kendoGrid:
{
data: RowList,
columns: ko.toJS(GridHeader.ColumnDefinitionList),
rowTemplate: 'kendoui-grid-row',
altRowTemplate: 'kendoui-alt-grid-row',
useKOTemplates: true,
scrollable: false,
pageable: { pageSize: 10 },
sortable: true
}"></div>
</script>
<script type="text/html" id="kendoui-grid-row">
<tr data-bind="visible: IsDeleted() == false, template: { name: 'grid-row' }"></tr>
</script>
<script type="text/html" id="kendoui-alt-grid-row">
<tr data-bind="visible: IsDeleted() == false, template: { name: 'grid-row' }" class="k-alt"></tr>
</script>
<script type="text/html" id="grid-row">
<td><div class="grid-button delete-row-button"><span class="icon-delete"></span></div></td>
<!-- ko foreach: CellList -->
<td data-bind="template: { name: Input.Type, data: Input }">
</td>
<!-- /ko -->
</script>
<script type="text/html" id="text">
<input class="text-input input k-textbox" data-bind="value: Value" />
</script>
In this case, I have a grid filled with text boxes (in each cells of the grid, there is an input of type text). Now my problem is that sorting by clicking the grid headers does nothing even if there is texts in the inputs. What I would like to do is be able to define the target of the sorting. In this case it would be the value of the input inside the grid cells.
I've ran into a similar problem a while ago. The problem was that the Kendo Grid doesn't work well with ko observables. As a workaround you can create a computed which returns the data as a plain JS object:
this.RowList.asJS = ko.computed(function() {
return ko.toJS(this.RowList());
}, this);
Then change data: RowList with the following:
data: RowList.asJS
This however means you lose the observables in your row templates. A workaround for this is to get the original object again inside your row.
First create a helper function to quickly get an item by ID (assuming you have an Id property):
this.RowList.getById = function(id) {
return ko.utils.arrayFirst(this.RowList(), function(row) {
return ko.unwrap(row.Id) === id;
});
}, this);
Now you can call getById inside your row templates:
<script type="text/html" id="kendoui-grid-row">
<!-- ko with: $root.RowList.getById(Id) -->
<tr data-bind="visible: IsDeleted() == false, template: { name: 'grid-row' }"></tr>
<!-- /ko -->
</script>

Kendo mobile template styling/formatting not working

I am trying to use a template as shown below, the outcome is a view with all elements from the template on one line, even though i am using to separate the elements. Why does this not display properly? It seems that no matter what styling i do it still ends up a single line view.
UPDATE
The culprit is the kendo style sheet - kendo.mobile.all.min.css -
So the new question for a kendo expert is why does kendo handle input fields differently when they appear in a listview via a template than when they appear outside of a template?
An input field outside of a listview template gets this class
.km-ios .km-list input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="image"]):not([type="checkbox"]):not([type="radio"]):not(.k-input):not(.k-button), .km-ios .km-list select:not([multiple]), .km-ios .km-list .k-dropdown-wrap, .km-ios .km-list textarea
Which results in no odd styling rules :) Normal text field view
An input field inside of the template gets this class
.km-root input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="image"]):not([type="checkbox"]):not([type="radio"]):not(.k-input):not(.k-button), .km-root select:not([multiple]), .km-root .k-dropdown, .km-root textarea
which results in these rules being applied to it (making the field sit in a wierd spot and loose all normal field stlying ie border background etc.) Im not 100% sure which wrapper is causing this
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
font-size: 1.1rem;
color: #385487;
min-width: 6em;
border: 0;
padding: .4em;
outline: 0;
background:
transparent;
My work around is to give any text fields inside listview templates the class="k-input" which obviously excludes them from the above css -
<script src="kendo/js/jquery.min.js"></script>
<script src="kendo/js/kendo.mobile.min.js"></script>
<link href="kendo/styles/kendo.mobile.all.min.css" rel="stylesheet" />
<!-- eventDetail view -------------------------------------------------------------------------------------------------->
<div data-role="view" id="view-eventDetail" data-show="getEventDetailData" data-title="eventDetail">
<header data-role="header">
<div data-role="navbar">
<span data-role="view-title"></span>
<a data-align="right" data-role="button" class="nav-button" href="#view-myEvents">Back</a>
</div>
</header>
<form id="updateEventForm">
<div id="updateEvent">
<div id="eventDetail"></div>
<p>
<input type="button" id="eventUpdateCancelButton" style="width:30%" data-role="button" data-min="true" value="Back" />
<input type="submit" id="eventUpdateSaveButton" style="width:30%" data-role="button" data-min="true" value="Save" />
</p>
<div id="eventResult"></div>
</div>
</form>
</div>
<script id="eventDetail-template" type="text/x-kendo-template">
<p>
<input name="event_type" id="event_type" data-min="true" type="text" value="#= type #" />
</p>
<p>
<input name="event_loc" id="event_loc" data-min="true" type="text" value="#= type #" />
</p>
<p>
<input name="event_date_time" id="event_date_time" data-min="true" type="datetime" value="#= stamp #" />
</p>
<p>
Share this
<input data-role="switch" id="event_share" data-min="true" checked="checked" value="#= share #"/>
</p>
<input name="userID" id="userID" type="hidden" value="#= user_id #" />
<input name="eventID" id="eventID" type="hidden" value="#= event_id #" />
</script>
<script>
function getEventDetailData(e) {
var dataSource = new kendo.data.DataSource({
transport: {
read: {
url: "http://localhost/mpt/website/api/event_details.php",
dataType: "jsonp",
type: "GET",
data: { userID: e.view.params.user_id, eventID: e.view.params.event_id },
cache: false
},
parameterMap: function(options) {
return {
userID: options.userID,
eventID: options.eventID
};
}
},
schema: { // describe the result format
data: "results" // the data which the data source will be bound to is in the "results" field
}
});
console.log(e);
$("#eventDetail").kendoMobileListView({
dataSource: dataSource,
template: kendo.template($("#eventDetail-template").html())
}).data("kendoMobileListView");
}
//update event
function sendUpdateEvent() {
var siteURI = "http://localhost/mpt/website/api/update_event.php?";
app.showLoading();
var user_id = $('#userID').val();
var event_id = $('#eventID').val();
var event_type = $('#event_type').val();
var event_loc = $('#event_loc').val();
var event_date_time = $('#event_date_time').val();
var event_share = $('#event_share').val();
var formVals = 'eventID=' + event_id + '&userID=' + user_id + '&event_type=' + event_type + '&event_loc=' + event_loc + '&event_date_time=' + event_date_time + '&event_share=' + event_share;
var fullURI = siteURI + formVals;
$.ajax({
url: fullURI, dataType: 'json', success: function (data) {
$('#eventResult').html(data.results);
app.hideLoading();
app.navigate("#view-myEvents");
}
});
}
$('#eventUpdateCancelButton').click(function () {
app.navigate("#view-myEvents");
});
$('#eventUpdateSaveButton').click(function () {
sendUpdateEvent();
});
$('#updateEventForm').submit(function () {
sendUpdateEvent();
return false;
});
</script>
ListView widgets are supposed to be applied to <ul> elements.
Try changing:
<div id="eventDetail"></div>
to:
<ul id="eventDetail"></ul>
Also with this bit of code:
$("#eventDetail").kendoMobileListView({
dataSource: dataSource,
template: kendo.template($("#eventDetail-template").html())
}).data("kendoMobileListView");
The .data() call on the end isn't doing anything here and can be removed, and also you can pass just the text string as the template. You don't need to call kendo.template() yourself. So you can change that to just:
$("#eventDetail").kendoMobileListView({
dataSource: dataSource,
template: $("#eventDetail-template").html()
});

Resources