Special binding in knockout that renders one template in another - data-binding

I am trying to solve a problem of rendering one template in context of another template with knockout. The outer template doesn't know and shouldn't care about the inner template and its view model. All it cares about is it's own template, a place to embed the inner template passing the name of it and its view model.
So ideally I wish I know how to implement the following binding:
<script type="text/html" id="outerTemplate">
<div class="outer-template" data-bind="here: {}"></div>
</script>
<!-- ko nested: { to: 'outerTemplate', data: { name: 'I am an inner view model' } } -->
<div class="inner-template" data-bind="text: name"></div>
<!-- /ko -->
If anyone knew the knockout well enough to easily outline such kind of binding I would greatly appreciate it.
UPDATE: The feature request was proposed: https://github.com/knockout/knockout/issues/1251

The template binding allows you to dynamically select the template name to use, so you can do something like:
<script id="outer" type="text/html">
<h2>Outer</h2>
<div data-bind="template: { name: tmplName, data: data }"></div>
</script>
<script id="inner" type="text/html">
<h3>Inner</h3>
<input data-bind="value: name" />
</script>
<div data-bind="template: 'outer'"></div>
In this case the view model would look like:
var vm = {
tmplName: 'inner',
data: {
name: ko.observable("Bob")
}
};
ko.applyBindings(vm);
The view model could be structured however you want. The key is just that you are passing the template name and data into the template binding.
Sample: http://jsfiddle.net/rniemeyer/LHhc8/

There is a working example I made myself: http://jsfiddle.net/m34wp/4/
var templateComputedDomDataKey = '__ko__templateComputedDomDataKey__';
function disposeOldComputedAndStoreNewOne(element, newComputed) {
var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
if(oldComputed && (typeof (oldComputed.dispose) == 'function')) {
oldComputed.dispose();
}
ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && newComputed.isActive()) ? newComputed : undefined);
}
function makeArray(arrayLikeObject) {
var result = [];
for(var i = 0, j = arrayLikeObject.length; i < j; i++) {
result.push(arrayLikeObject[i]);
}
;
return result;
}
function moveCleanedNodesToContainerElement(nodes) {
var nodesArray = makeArray(nodes);
var container = document.createElement('div');
for(var i = 0, j = nodesArray.length; i < j; i++) {
container.appendChild(ko.cleanNode(nodesArray[i]));
}
return container;
}
ko.bindingHandlers['nested'] = {
'init': function (element, valueAccessor) {
var elementType = 1;
var commentType = 8;
var bindingValue = ko.utils.unwrapObservable(valueAccessor());
if(element.nodeType == elementType || element.nodeType == commentType) {
// It's an anonymous template - store the element contents, then clear the element
var templateNodes = element.nodeType == 1 ? element.childNodes : ko.virtualElements.childNodes(element);
var container = moveCleanedNodesToContainerElement(templateNodes);
new ko.templateSources.anonymousTemplate(element)['nodes'](container);
}
return {
'controlsDescendantBindings': true
};
},
'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var options = ko.utils.unwrapObservable(valueAccessor());
var outerTemplateName = options['to'];
var dataValue = ko.utils.unwrapObservable(options['data']) || viewModel;
var innerContext = bindingContext['createChildContext'](dataValue);
innerContext.innerTemplateElement = element;
var templateComputed = ko.renderTemplate(outerTemplateName, innerContext, options, element);
disposeOldComputedAndStoreNewOne(element, templateComputed);
}
};
ko.bindingHandlers['here'] = {
'init': function (element, valueAccessor) {
return {
'controlsDescendantBindings': true
};
},
'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var templateElement = bindingContext.innerTemplateElement;
if(viewModel != null) {
var innerContext = bindingContext['createChildContext'](viewModel);
var templateComputed = ko.renderTemplate(templateElement, innerContext, {
}, element);
disposeOldComputedAndStoreNewOne(element, templateComputed);
} else {
}
}
};
ko.virtualElements.allowedBindings['nested'] = true;

Related

How to hide server controls untill angular load custom multi select dropdown

i am working with asp.net webform and angular.i have few asp.net server side controls in page and i have used a angular multi select dropdown which invoke server side function to get data. so when i am running my application then server side controls comes so fast but later angular multi select display which look odd.
so please see my code and suggest me how to refactor my code to sync the server side and client side UI appear at same time.
here is bit of my server side code
public ActionResult MultiSelectCountry()
{
ViewBag.Countries = GetCountries();
return View();
}
[NonAction]
public JsonResult GetCountries()
{
List<Country> oList = new List<Country>()
{
new Country {ID=1,Name="United Kingdom"},
new Country {ID=1,Name="United States"},
new Country {ID=1,Name="Italy"},
new Country {ID=1,Name="Germany"},
new Country {ID=1,Name="India"}
};
return Json(oList);
}
here is bit of my client side code
<body ng-app="app" ng-controller="MainCtrl">
<div class="col-sm-10 col-sm-offset-1">
<searchable-multiselect display-attr="Name"
selected-items="SelectedCountryList" all-items="CountryList"
add-item="addLanguageToUser(item)" remove-item="removeLanguageFromUser(item)" >
</searchable-multiselect>
</div>
<script data-require="jquery" data-semver="2.1.3" src="http://code.jquery.com/jquery-2.1.3.min.js"></script>
<script data-require="bootstrap" data-semver="3.3.2" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
<script data-require="angular.js#1.3.x" src="https://code.angularjs.org/1.3.14/angular.js" data-semver="1.3.14"></script>
<script data-require="ui-bootstrap" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.0/ui-bootstrap-tpls.min.js" data-semver="0.13.0"></script>
<script>
var _countries = #Html.Raw(Json.Encode(ViewBag.Countries));
var app = angular.module('app', ["ui.bootstrap"]);
var yourApp = yourApp || {};
yourApp.Settings = yourApp.Settings || {};
yourApp.Settings.BaseUrl = "#Url.Content("~")";
yourApp.Settings.CountryList = _countries["Data"];
angular.module("app").value("appSettings", yourApp);
</script>
<script src="~/Scripts/app.js"></script>
</body>
app.js code
app.controller('MainCtrl', function ($scope, appSettings) {
$scope.CountryList = appSettings.Settings.CountryList;
$scope.SelectedCountryList = [];
$scope.addLanguageToUser = function (country) {
$scope.SelectedCountryList.push(country)
};
$scope.removeLanguageFromUser = function (country) {
var idx = $scope.SelectedCountryList.indexOf(country);
$scope.SelectedCountryList.splice(idx, 1);
};
});
app.directive("searchableMultiselect", function ($timeout, appSettings) {
return {
templateUrl: appSettings.Settings.BaseUrl + 'Angular/Directives/searchableMultiselect.html',
restrict: 'AE',
scope: {
displayAttr: '#',
selectedItems: '=',
allItems: '=',
readOnly: '=',
addItem: '&',
removeItem: '&'
},
link: function (scope, element, attrs) {
element.bind('click', function (e) {
e.stopPropagation();
});
scope.width = element[0].getBoundingClientRect();
scope.updateSelectedItems = function (obj) {
var selectedObj;
for (i = 0; typeof scope.selectedItems !== 'undefined' && i < scope.selectedItems.length; i++) {
if (scope.selectedItems[i][scope.displayAttr].toUpperCase() === obj[scope.displayAttr].toUpperCase()) {
selectedObj = scope.selectedItems[i];
break;
}
}
if (typeof selectedObj === 'undefined') {
scope.addItem({ item: obj });
} else {
scope.removeItem({ item: selectedObj });
}
};
scope.isItemSelected = function (item) {
if (typeof scope.selectedItems === 'undefined') return false;
var tmpItem;
for (i = 0; i < scope.selectedItems.length; i++) {
tmpItem = scope.selectedItems[i];
if (typeof tmpItem !== 'undefined'
&& typeof tmpItem[scope.displayAttr] !== 'undefined'
&& typeof item[scope.displayAttr] !== 'undefined'
&& tmpItem[scope.displayAttr].toUpperCase() === item[scope.displayAttr].toUpperCase()) {
return true;
}
}
return false;
};
scope.commaDelimitedSelected = function () {
var list = "";
angular.forEach(scope.selectedItems, function (item, index) {
list += item[scope.displayAttr];
if (index < scope.selectedItems.length - 1) list += ', ';
});
return list.length ? list : "Nothing Selected";
}
}
}
});
my request is modify my code in such way as a result server side controls will be visible only when multi select is ready to show. all UI related things should be visible at same time. thanks
Something like
<div class="col-sm-10 col-sm-offset-1" ng-show="MainCtrl.CountryList.length>0"> should probably do the trick.

MeteorJS: Collection.find fires multiple times instead of once

I have an app that when you select an industry from a drop down list a collection is updated where the attribute equals the selected industry.
JavaScript:
Template.selector.events({
'click div.select-block ul.dropdown-menu li': function(e) {
var selectedIndex = $(e.currentTarget).attr("rel");
var val = $('select#industryPicker option:eq(' + selectedIndex + ')').attr('value');
var oldVal = Session.get('currentIndustryOnet');
if(val != oldVal) {
Session.set('jobsLoaded', false);
Session.set('currentIndustryOnet', val);
Meteor.call('countByOnet', val, function(error, results){
if(results > 0) {
Session.set('jobsLoaded', true);
} else {
getJobsByIndustry(val);
}
});
}
}
});
var getJobsByIndustry = function(onet) {
if(typeof(onet) === "undefined")
alert("Must include an Onet code");
var params = "onet=" + onet + "&cn=100&rs=1&re=500";
return getJobs(params, onet);
}
var getJobs = function(params, onet) {
Meteor.call('retrieveJobs', params, function(error, results){
$('job', results.content).each(function(){
var jvid = $(this).find('jvid').text();
var job = Jobs.findOne({jvid: jvid});
if(!job) {
options = {}
options.title = $(this).find('title').text();
options.company = $(this).find('company').text();
options.address = $(this).find('location').text();
options.jvid = jvid;
options.onet = onet;
options.url = $(this).find('url').text();
options.dateacquired = $(this).find('dateacquired').text();
var id = createJob(options);
console.log("Job Created: " + id);
}
});
Session.set('jobsLoaded', true);
});
}
Template.list.events({
'click div.select-block ul.dropdown-menu li': function(e){
var selectedIndex = $(e.currentTarget).attr("rel");
var val = $('select#perPage option:eq(' + selectedIndex + ')').attr('value');
var oldVal = Session.get('perPage');
if(val != oldVal) {
Session.set('perPage', val);
Pagination.perPage(val);
}
}
});
Template.list.jobs = function() {
var jobs;
if(Session.get('currentIndustryOnet')) {
jobs = Jobs.find({onet: Session.get('currentIndustryOnet')}).fetch();
var addresses = _.chain(jobs)
.countBy('address')
.pairs()
.sortBy(function(j) {return -j[1];})
.map(function(j) {return j[0];})
.first(100)
.value();
gmaps.clearMap();
$.each(_.uniq(addresses), function(k, v){
var addr = v.split(', ');
Meteor.call('getCity', addr[0].toUpperCase(), addr[1], function(error, city){
if(city) {
var opts = {};
opts.lng = city.loc[1];
opts.lat = city.loc[0];
opts.population = city.pop;
gmaps.addMarker(opts);
}
});
})
return Pagination.collection(jobs);
} else {
jobs = Jobs.find()
Session.set('jobCount', jobs.count());
return Pagination.collection(jobs.fetch());
}
}
In Template.list.jobs if you console.log(addresses), it is called 4 different times. The browser console looks like this:
(2) 100
(2) 100
Any reason why this would fire multiple times?
As #musically_ut said it might be because of your session data.
Basically you must make the difference between reactive datasources and non reactive datasources.
Non reactive are standard javascript, nothing fancy.
The reactive ones however are monitored by Meteor and when one is updated (insert, update, delete, you name it), Meteor is going to execute again all parts which uses this datasource. Default reactive datasources are: collections and sessions. You can also create yours.
So when you update your session attribute, it is going to execute again all helper's methods which are using this datasource.
About the rendering, pages were rendered again in Meteor < 0.8, now with Blaze it is not the case anymore.
Here is a quick example for a better understanding:
The template first
<head>
<title>test</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
<h1>{{getSession}}</h1>
<h1>{{getNonReactiveSession}}</h1>
<h1>{{getCollection}}</h1>
<input type="button" name="session" value="Session" />
<input type="button" name="collection" value="Collection" />
</template>
And the client code
if (Meteor.isClient) {
CollectionWhatever = new Meteor.Collection;
Template.hello.events({
'click input[name="session"]': function () {
Session.set('date', new Date());
},
'click input[name="collection"]': function () {
CollectionWhatever.insert({});
}
});
Template.hello.getSession = function () {
console.log('getSession');
return Session.get('date');
};
Template.hello.getNonReactiveSession = function () {
console.log('getNonReactiveSession');
var sessionVal = null;
new Deps.nonreactive(function () {
sessionVal = Session.get('date');
});
return sessionVal;
};
Template.hello.getCollection = function () {
console.log('getCollection');
return CollectionWhatever.find().count();
};
Template.hello.rendered = function () {
console.log('rendered');
}
}
If you click on a button it is going to update a datasource and the helper method which is using this datasource will be executed again.
Except for the non reactive session, with Deps.nonreactive you can make Meteor ignore the updates.
Do not hesitate to add logs to your app!
You can read:
Reactivity
Dependencies

Breeze.js query does not update knockout view

I am querying data via breeze.js which works fine the first time. The second time the view isn't updated.
html
<a id="linkQDate">Order by Newest</a>
<a id="linkQScore">Order by Score</a>
<div id="questionWrapper">
<ul data-bind="foreach: results">
<li>
<strong><span data-bind="text: Score"></span></strong>
<span data-bind="text: Titel"></span>
</li>
</ul>
</DIV>
js
$(document).ready(function () {
var manager = new breeze.EntityManager('/breeze/dbentities');
var isApplied = false;
var dmodel;
$("#linkQDate").click(function () {
var query = breeze.EntityQuery.from("Questions").orderBy("Date");
manager.executeQuery(query).then(querySucceeded);
function querySucceeded(data) {
dmodel = data;
if (!isApplied) {
ko.applyBindings(dmodel, $("#questionWrapper")[0]);
isApplied = true;
}
}
});
$("#linkQScore").click(function () {
var query = breeze.EntityQuery.from("Questions").orderBy("Score");
manager.executeQuery(query).then(querySucceeded);
function querySucceeded(data) {
dmodel = data;
if (!isApplied) {
ko.applyBindings(dmodel, $("#questionWrapper")[0]);
isApplied = true;
}
}
});
});
If you just using plain JS objects it will not work. Because it does not know when underlying data changes. Use mapping plugin for this to work:
$(document).ready(function () {
var manager = new breeze.EntityManager('/breeze/dbentities');
var isApplied = false;
var dmodel;
function querySucceeded(data) {
if (!isApplied) {
dmodel = ko.mapping.fromJS(data);
ko.applyBindings(dmodel, $("#questionWrapper")[0]);
isApplied = true;
} else {
ko.mapping.fromJS(data, dmodel);
}
}
$("#linkQDate").click(function () {
var query = breeze.EntityQuery.from("Questions").orderBy("Date");
manager.executeQuery(query).then(querySucceeded);
});
$("#linkQScore").click(function () {
var query = breeze.EntityQuery.from("Questions").orderBy("Score");
});
});
Working code from Tomas without mappings. It is much faster:
$(document).ready(function () {
var manager = new breeze.EntityManager('/breeze/dbentities');
var isApplied = false;
var dmodel = { results: ko.observableArray() };
function queryFailed(data) {
console.log(data);
}
function querySucceeded(data) {
if (!isApplied) {
for (var i = 0; i < data.results.length; i++) {
dmodel.results.push(data.results[i]);
}
ko.applyBindings(dmodel, $("#questionWrapper")[0]);
isApplied = true;
} else {
dmodel.results.removeAll();
for (var i = 0; i < data.results.length; i++) {
dmodel.results.push(data.results[i]);
}
}
}
$("#linkQDate").click(function () {
var query = breeze.EntityQuery.from("Questions").orderBy("Date");
manager.executeQuery(query).then(querySucceeded).fail(queryFailed);;
});
$("#linkQScore").click(function () {
var query = breeze.EntityQuery.from("Questions").orderBy("Score");
manager.executeQuery(query).then(querySucceeded).fail(queryFailed);;
});
});

KnockoutJS: How to color table cell (+fade away)

I'm discovering the awesome KnockoutJS library and I'm stuck on a feature I would like to implement:
Explanation
I have an array containing n lines of a type of object:
Html:
<tbody data-bind="foreach: indexItems">
<tr data-bind="click: $parent.UpdateInterfaceItems, css: { 'active-row' : $root.selecteditem() === $data }">
<td data-bind="text: param1"></td>
<td data-bind="text: param2"></td>
<td data-bind="text: param3"></td>
</tr>
</tbody>
Javascript:
function ViewModel() {
var self = this;
//Public Properties
self.selecteditem = ko.observable();
self.indexMats = ko.observableArray();
....
hub.client.receivedNewValue= function (param1Value, param2Value, param3Value)
{
var match = ko.utils.arrayFirst(vm.indexItems(), function (item) {
return item.param1() == param1Value;
});
if (match)
{
match.param1(param1Value);
match.param2(param2Value);
match.param3(param3Value);
}
}
}
Feature
Sometimes I want to update one row (on some values only) and I would like to highlight the modified cell with a color which would fade away. Is there any way to do that?
I've found out a quite similar question but it doesn't match my need (Knockout JS Update Color)
Thanks a lot
I have written a binding name highlight long time ago. Think that is smilar to what you need.
ko.bindingHandlers.highlight = {
update: function(element, valueAccessor) {
ko.toJS(valueAccessor());
var old = parseInt($(element).html(),10);
var current = parseInt(valueAccessor()(),10);
if ($(element).data("ko_init")) {
if(current < old) {
$(element).effect('highlight', {
color: '#AA0000'
}, 1000);
} else if (current>old ) {
$(element).effect('highlight', {
color: '#00AA00'
}, 1000);
}
}
else {
$(element).data("ko_init", true);
}
}};
function Table(rowCount, columnCount, headItems, initiliaze) {
var self = this;
var Cell = function(data, css, animate) {
var cellInstance = this;
this.data = ko.observable(data);
this.css = ko.observable(css);
this.animate = ko.observable(animate);
}
this.rowCount = ko.observable(rowCount);
this.columnCount = ko.observable(columnCount);
this.headItems = ko.observableArray(headItems);
this.rowsData = ko.observableArray();
if (initiliaze) {
for (var i = 0; i < rowCount; i++) {
var tmpArray = new Array();
for (var j = 0; j < columnCount; j++)
tmpArray.push(new Cell("0", "", ""));
self.rowsData.push(ko.observableArray(tmpArray));
}
}}
function ViewModel() {
var self = this;
this.table = new Table(5, 4, ["Name", "DT", "BID", "ASK"], true);
};
$(function() {
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
function changeData(x, y, data) {
viewModel.table.rowsData()[x]()[y].data(data);
}
setInterval(function() {
changeData(parseInt(Math.random(1000) * 5, 10), parseInt(Math.random(1000) * 4, 10), parseInt(Math.random(1000) * 10000, 10));
}, 1000);
})
Check out jsfiddle demo

Changing the value of a Telerik RadEditor with Javascript/jQuery

I'm trying to manually clean the HTML of a Telerik RadEditor with Javascript but I can't seem to find the correct place to store the value so that it gets saved on post back.
Here's the JS I have:
$(function () {
jQuery.fixHash = function ($html) {
// modify $html
return $html;
};
$("#adminEditingArea input[id$='SaveButton']").unbind("click").click(function () {
$("iframe[id$='_contentIframe']").trigger("save");
// call .net postback
return false;
});
});
var editorSaveEventInit = false;
function InitSaveEvent() {
if (!editorSaveEventInit) {
var $EditFrames = $("iframe[id$='_contentIframe']");
if ($EditFrames && $EditFrames.length > 0) {
$EditFrames.bind("save", function (e) {
var $thisFrame = $(this);
var thisFrameContents = $thisFrame.contents();
if (thisFrameContents) {
var telerikContentIFrame = thisFrameContents.get(0);
var $body = $("body", telerikContentIFrame);
var html = $.fixHash($body).html();
$body.html(html);
// also tried storing the modified HTML in the textarea, but it doesn't seem to save:
//$thisFrame.prev("textarea").html(encodeURIComponent("<body>" + html + "</body>"));
}
});
editorSaveEventInit = true;
}
}
};
$(window).load(function () {
InitSaveEvent();
});
Is there any way to access the Telerik RadEditor object with JavaScript (using OnClientCommandExecuted()?) so that I can access the .get_html() and .set_html(value) functions? If not, what values do I need to set before posting back?
Why don't you use custom content filters.
Ah, just discovered Telerik's built-in $find() function: http://www.telerik.com/help/aspnet-ajax/editor_getingreferencetoradeditor.html
Edit: here's the solution I came up with for my InitSaveEvent() function:
var editorSaveEventInit = false;
function InitSaveEvent() {
if (!editorSaveEventInit) {
var $EditFrames = $("iframe[id$='_contentIframe']");
if ($EditFrames && $EditFrames.length > 0) {
$EditFrames.bind("save", function (e) {
var $thisFrame = $(this);
var thisFrameContents = $thisFrame.contents();
if (thisFrameContents) {
var telerikContentIFrame = thisFrameContents.get(0);
var $body = $("body", telerikContentIFrame);
var html = $.fixHash($body).html();
// SOLUTION!
var $radeditor = $thisFrame.parents("div.RadEditor.Telerik:eq(0)");
var editor = $find($radeditor.attr("id"));
editor.set_html(html);
// ☺
}
});
editorSaveEventInit = true;
}
}
};

Resources