Dynamically bind a knockout component with a cshtml template breaks the viewmodel - asp.net-mvc

I have an issue with knockout components in our SPA, on a certain page I'm trying to implement a component; all observables passed through the params attribute are working as they should. But when I create ko.observable(), ko.computed, .. in the component's viewmodel they just don't work as they should. (Btw we're working with asp.net mvc and we need to serve cshtml files as the templates, the razor in the views helps us with several parsing options)
So getting the files are not directly the issue, what is weird is that in the cshtml file, i can't use the unwrapped text: test, I have to write text: test(). computeds aren't updated either, nothing seems to do what it should.
What I think I'm missing is that the component is never added through ko.applyBindings() but that's purely a guess. can anybody shed some light on this please?
What to do with adding a knockout component after page has already loaded. And yes I've searched around, been fiddling all day without avail.. thanks in advance.
This is in our mainpage.
<track-item-list params="value: data, selected: selectedItems"></track-item-list>
Below within script tags:
if (!ko.components.isRegistered('track-item-list')) {
ko.components.register('track-item-list', { require: '#Url.Content("~/Scripts/viewmodels/items/_cItemList.js")' });
}
There's a viewmodel js file:
define(['knockout', 'cshtml!/items/_cItemList'], function (ko, cshtmlString) {
function ItemViewModel(params) {
var self = this;
self.selected = params.selected;
self.test = ko.observable('Hello World!');
}
return { viewModel: ItemViewModel, template: cshtmlString };
});
cshtml template:
<span data-bind="text: selected().length"></span>
<ul data-bind="foreach: selected">
<li data-bind="text: Total"></li>
</ul>
<span data-bind="text: test"></span>
The cshtml file is loaded via a custom script:
define(['jquery'], function($) {
var buildText = {};
return {
load: function (name, req, onLoad, config) {
var self = this,
file = name,
segments = file.split('/');
// If the module name does not have an extension, append the default one
if (segments[segments.length - 1].lastIndexOf('.') == -1) {
file += '.cshtml';
}
$.get(name, function (html, status, xhr) {
console.log('html loaded: ' + name);
onLoad(html);
});
}
};
});

Related

How to put kendo validator message span element underneath the input being validated?

I'm trying to figure this out for a while.
All the examples I saw, use the html with input and span elements manually inserted
I have the following code that generate form and its datepicker elements dynamically:
#using (Html.BeginForm("Reload", "FileDate", FormMethod.Post, new { returnUrl = this.Request.RawUrl, id = "DateForm", onsubmit = "return ValidateDate();" } ))
{
#(Html.Kendo().DatePicker()
.Name("Date")
.Value(Session["FileDate"] == null ? DateTime.Now : Convert.ToDateTime(Session["FileDate"].ToString()))
.Events(e => e
.Change("datepicker_change")
)
)
#Html.Hidden("returnUrl", this.Request.RawUrl)
<script>
function datepicker_change() {
if(ValidateDate()){
$("#DateForm").submit();
}
}
</script>
}
When form is generated, I have the following code on the page:
This is a validation:
<script>
$(document).ready(function() {
$("#mainMenu").kendoMenu();
$("#Date").attr('required', 'required');
$("#Date").attr('data-WrongFormat-msg', 'Date Format is Wrong');
var validator = $("#container").kendoValidator({
rules: {
WrongFormat: function (input) {
if (input.is("[data-role=datepicker]")) {
var dateBox = input.data("kendoDatePicker");
return input.data("kendoDatePicker").value();
} else {
return true;
}
}
}
})
});
function ValidateDate()
{
var validator = $("#container").data("kendoValidator");
if (validator.validate()) {
return true;
}
else
{
return false;
}
}
</script>
When I provide the incorrect input or no input at all, I get the correct message in the span. However, this span section modifies the layout of the page:
How can I fix that, so my error span is placed underneath my form, the way it is shown in some examples like here: http://dojo.telerik.com/ikUfu:
I have the exact same issue. The problems seems to be that the validation message span element is in the wrong place for DatePickers. It is inside this element:
but, in all other widgets, it's inside the spam element one level higher:
So, it seems this is a bug in Telerik at the moment. It works for other widgets, but not for DatePicker. I'll see and find if this bug is already reported, and if not, report it. If you desperately need it fixed asap, I assume you could try some jquery magic to move the span element.
I've faced it too. Is there any proper solution for this issue?
My workaround in a nutshell:
Delete all possible old error messages
Move the new one into the proper HTML container
<div id="div_id">
<input id="input_id" type="text">
</div>
<script>
var validatable = $("[id='input_id'").kendoValidator({
rules: {
minimumLengthRule: function (input) {
var trimmedInputValue = $.trim(input.val());
return trimmedInputValue.length > 0 ;
}
},
messages: {
minimumLengthRule: "The input length is too short."
}
}).data("kendoValidator");
validatable.bind("validateInput", function (e) {
$("#div_id > span").not(':first').remove(); // 1.
if (!e.valid) {
$("[id='input_id_validationMessage'").appendTo('#div_id'); // 2.
}
});
</script>

Knockout Binding Not Working with jQueryUI Dialogue

My viewModel has an array called 'Items'. I want to display the contents of 'Items' using a foreach binding. Everything works fine when I use regular HTML. But does not work with a dialogue box which I created using jQueryUI.
HTML:
<div id="skus0">
<div id="skus1">
<ul data-bind="foreach: Items">
<li data-bind="text:Name"></li>
</ul>
</div>
<input type="button" id="openQryItems" class="btn btn-info" value="Open" data-bind="click:openQueryItems" />
</div>
JavaScript:
// my view model
var viewModel = {
Items: [{Name:'Soap'},{Name:'Toothpaste'}]
};
// JS to configure dialogue
$("#skus1").dialog({
autoOpen: false,
width: 500,
modal: true,
buttons: {
"OK": function () {
$(this).dialog("close");
},
"Cancel": function () {
$(this).dialog("close");
}
}
});
// for mapping my model using ko.mapping plugin
var zub = zub || {};
zub.initModel = function (model) {
zub.cycleCountModel = ko.mapping.fromJS(model);
zub.cycleCountModel.openQueryItems = function () {
$("#skus1").dialog("open");
}
ko.applyBindings(zub.cycleCountModel, $("#skus0")[0]);
}
zub.initModel(viewModel);
I have created a fiddle here my fiddle
$.fn.dialog removes the element from its place in the DOM and places it in a new container; this is how it can create a floating window. The problem with this happening is that it breaks data binding, since the dialog DOM is no-longer nested within the top-level data-bound DOM.
Moving the dialog initialization to after ko.applyBindings will enable dialog to yank stuff out of the DOM after the list is populated. Of course, this means that after that point, future changes will still not be reflected, which may be important if you're wanting the opened dialog to change automatically.
If you are wanting the dialog contents to be fully dynamic, you could create a binding handler; we did this in our project. Here's a rough outline of how we did this:
ko.bindingHandlers.dialog = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingCtx) {
var bindingValues = valueAccessor();
var hasAppliedBindings = false;
var elem = $(element);
var options = {
id: ko.utils.unwrapObservable(bindingValues.id),
title: ko.utils.unwrapObservable(bindingValues.title),
// etc...
onOpen: function () {
if (!hasAppliedBindings) {
hasAppliedBindings = true;
var childCtx = bindingCtx.createChildContext(viewModel);
ko.applyBindingsToDescendants(childCtx, element);
}
}
};
elem.dialog(options);
}
return { controlsDescendantBindings: true };
}
...which we used like this:
<div data-bind="dialog: { title: 'some title', id: 'foo', ... }">
<!-- dialog contents -->
</div>
What return { controlsDescendantBindings: true } does is makes sure that outer bindings do not affect anything using the dialog binding handler. Then we create our own Knockout binding "island" after it is pulled out of the DOM, based on the original view model.
Although in our project we also used hybrid jQuery+Knockout, I would highly recommend you avoid this whenever possible. There were so many hacks we had to employ to sustain this type of application. The very best thing you should do is prefer Knockout binding handlers (and I think it has a "component" concept now which I haven't played with) over DOM manipulations to avoid buggy UI management.

Initializing knockoutjs content in child jquery mobile page

I know there are a lot of questions that cover jquery mobile / knockoutjs integration, however I couldn't find a thread that solved my issue. I have a master view model which contains child view models, and so I initialize this on page load, as that event is only fired on application load:
var viewModel = null;
$(function () {
console.debug("running init");
viewModel = new ViewModel();
ko.applyBindings(viewModel);
});
This works great on the first page of my app, however when I go to a child page, the knockoutjs content doesn't show up because jquery mobile has loaded the html dynamically and knockout doesn't know to update the binded content. I'm trying to tell it to update dynamically by using the $(document).delegate function, however I'm struggling with how it's supposed to be implemented.
<ul id="speeding" data-role="listview" data-bind="foreach: speeding.items">
<li>
<h3 class="ui-li-heading" data-bind="text: Address"></h3>
<p class="ui-li-desc" data-bind="text: Address2"></p>
<p class="ui-li-desc" data-bind="text: PrettyDate"></p>
<p class="ui-li-aside" data-bind="text: SpeedMph"></p>
</li>
<script type="text/javascript">
var loaded = false;
$(document).delegate("#page-speeding", "pagebeforecreate", function () {
if (!loaded) {
loaded = true;
ko.applyBindings(viewModel);
}
else {
$("#speeding").trigger("refresh");
}
});
</script>
</ul>
I'm putting the delegate function within the page it's being called on, as apparently that's a requirement of using delegate. Then on first load of this child page I call ko.applyBindings (I only wanted to call this on application load but I couldn't get trigger("create") to work. On subsequent calls it would call trigger("refresh") (which doesn't work for me.) The issue though is that the delegate function gets added each time I go to the child page. So on first load of the child page, it will call the delegate callback function once. If I go back to the main page, then back to the child page, it will call the delegate callback twice, and so on.
Can someone please provide guidance of the recommended approach to refreshing the knockoutjs bindings on child pages?
This is what ended up working for me. I have no idea if there's a better way or not...
var viewModel = null;
$(function () {
console.debug("running init");
viewModel = new ViewModel();
ko.applyBindings(viewModel);
var pages = [
"scorecard", "speeding", "leaderboard"
];
_.each(pages, function (page) {
$(document).on("pagebeforecreate", "#page-" + page, function () {
console.debug("applying " + page + " bindings");
ko.applyBindings(viewModel, $("#page-" + page)[0]);
});
});
});

Having trouble with jQueryUI accordion and Knockoutjs

I've been able to replicate the problem here: http://jsfiddle.net/NE6dm/
I have the following HTML which I'm using in an app:
<div data-bind="foreach: items, jqAccordion: { active: false, collapsible: true }">
<h3>
</h3>
<div>
hello
</div>
</div>
<button title="Click to return to the complaints list." data-bind="click: addItem">Add Item</button>
The idea is to display an accordion for a bunch of items that will dynamically be added/removed via a Knockout observable array.
Here's some JavaScript code which I use:
// Tab.
var tab = function (questionSet) {
this.id = questionSet.code;
this.title = questionSet.description;
this.questionSet = questionSet;
};
Custom Knockout binding handler:
ko.bindingHandlers.jqAccordion = {
init: function (element, valueAccessor) {
var options = valueAccessor();
$(element).accordion(options);
$(element).bind("valueChanged", function () {
ko.bindingHandlers.jqAccordion.update(element, valueAccessor);
});
},
update: function (element, valueAccessor) {
var options = valueAccessor();
$(element).accordion('destroy').accordion(options);
}
};
var NonSequentialViewModel = function () {
var items = ko.observableArray();
items.push(new tab({ id: 23, description : 'Added Inline' }));
var addItem = function() {
items.push(new tab({ id: 5, description: 'Added by a click' }));
};
return {
addItem: addItem,
items: items
}
}
var nonsequentialViewModel = new NonSequentialViewModel();
ko.applyBindings(nonsequentialViewModel);
Now the problem is this - when I view the HTML page, the item 'Added Inline' appears fine, in that I can collapse and expand it. However, when I click the button 'Add Item', a new item is added to he accordion, but it has not styling at all. For example:
In the above image, the first item is styled correctly, however the remaining items have none of the jQuery UI styling applied. Basically, any item which is added dynamically does not have any accordion styling applied.
I have seen this question
knockout.js and jQueryUI to create an accordion menu
and I've tried using the jsFiddle included in the question, but I cannot see why my code doesn't have the same result.
I'm hoping someone else has experienced this before and can help.
EDIT:
I've looked into this further and see that the problem is this - when I add a new item to the oservable array, the custom handler's update method is not executed. Thus the redrawing of the accordion never happens.
I can't see why the update should not be called. This is realyl doing my head in! :)
EDIT:
I've been able to replicate the problem here: http://jsfiddle.net/NE6dm/
Your NonSequentialViewModel constructor doesn't return items array. Update return statement to this:
return {
items: items,
addItem: addItem
}
Here is working fiddle: http://jsfiddle.net/vyshniakov/MfegM/323/
Old question, but I believe I was having the same problem.
I may need to submit a bug to knockout.js. I just spend several hours trying to figure out similar problems.
In short... if I load your jsfiddle and change the version of knockout to 2.1.0, it appears to work fine.
this:
<script type="text/javascript" src="http://cloud.github.com/downloads/SteveSanderson/knockout/knockout-2.2.0.debug.js"></script>
to this:
<script type="text/javascript" src="http://cloud.github.com/downloads/SteveSanderson/knockout/knockout-2.1.0.debug.js"></script>
(The only difference being the version 2.2.0 --> 2.1.0)
Further... I ended up settling on several versions:
jquery: 1.9.1
jquery-ui (combined): 1.9.2
knockoutjs: 2.1.0

knockout.js and jQueryUI to create an accordion menu

Got a slight problem trying to have jquery UI and knockout js to cohoperate. Basically I want to create an accordion with items being added from knockout through a foreach (or template).
The basic code is as follows:
<div id="accordion">
<div data-bind="foreach: items">
<h3></h3>
<div><a class="linkField" href="#" data-bind="text: link"></a></div>
</div>
</div>
Nothing impressive here... The problem is that if I do something like:
$('#accordion').accordion();
The accordion will be created but the inner div will be the header selector (first child, as default) so the effect is not the wanted one.
Fixing stuff with this:
$('#accordion').accordion({ header: 'h3' });
Seems to work better but actually creates 2 accordions and not one with 2 sections... weird.
I have tried to explore knockout templates and using "afterRender" to re-accordionise the div but to no avail... it seems to re-render only the first link as an accordion and not the second. Probably this is due to my beginner knowldge of jquery UI anyway.
Do you have any idea how to make everything work together?
I would go with custom bindings for such functionality.
Just like RP Niemeyer with an example of jQuery Accordion binding to knockoutjs http://jsfiddle.net/rniemeyer/MfegM/
I had tried to integrate knockout and the JQuery UI accordion and later the Bootstrap collapsible accordion. In both cases it worked, but I found that I had to implement a few workarounds to get everything to display correctly, especially when dynamically adding elements via knockout. The widgets mentioned aren't always aware of what is happening with regards to knockout and things can get messed up (div heights wrongly calculated etc...). Especially with the JQuery accordion it tends to rewrite the html as it sees fit, which can be a real pain.
So, I decided to make my own accordion widget using core JQuery and Knockout. Take a look at this working example: http://jsfiddle.net/matt_friedman/KXgPN/
Of course, using different markup and css this could be customized to whatever you need.
The nice thing is that it is entirely data driven and doesn't make any assumptions about layout beyond whatever css you decide to use. You'll notice that the markup is dead simple. This is just an example. It's meant to be customized.
Markup:
<div data-bind="foreach:groups" id="menu">
<div class="header" data-bind="text:name, accordion: openState, click: toggle"> </div>
<div class="items" data-bind="foreach:items">
<div data-bind="text:name"> </div>
</div>
</div>
Javascript:
ko.bindingHandlers.accordion = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
$(element).next().hide();
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var slideUpTime = 300;
var slideDownTime = 400;
var openState = ko.utils.unwrapObservable(valueAccessor());
var focussed = openState.focussed;
var shouldOpen = openState.shouldOpen;
/*
* This following says that if this group is the one that has
* been clicked upon (gains focus) find the other groups and
* set them to unfocussed and close them.
*/
if (focussed) {
var clickedGroup = viewModel;
$.each(bindingContext.$root.groups(), function (idx, group) {
if (clickedGroup != group) {
group.openState({focussed: false, shouldOpen: false});
}
});
}
var dropDown = $(element).next();
if (focussed && shouldOpen) {
dropDown.slideDown(slideDownTime);
} else if (focussed && !shouldOpen) {
dropDown.slideUp(slideUpTime);
} else if (!focussed && !shouldOpen) {
dropDown.slideUp(slideUpTime);
}
}
};
function ViewModel() {
var self = this;
self.groups = ko.observableArray([]);
function Group(id, name) {
var self = this;
self.id = id;
self.name = name;
self.openState = ko.observable({focussed: false, shouldOpen: false});
self.items = ko.observableArray([]);
self.toggle = function (group, event) {
var shouldOpen = group.openState().shouldOpen;
self.openState({focussed: true, shouldOpen: !shouldOpen});
}
}
function Item(id, name) {
var self = this;
self.id = id;
self.name = name;
}
var g1 = new Group(1, "Group 1");
var g2 = new Group(2, "Group 2");
var g3 = new Group(3, "Group 3");
g1.items.push(new Item(1, "Item 1"));
g1.items.push(new Item(2, "Item 2"));
g2.items.push(new Item(3, "Item 3"));
g2.items.push(new Item(4, "Item 4"));
g2.items.push(new Item(5, "Item 5"));
g3.items.push(new Item(6, "Item 6"));
self.groups.push(g1);
self.groups.push(g2);
self.groups.push(g3);
}
ko.applyBindings(new ViewModel());
Is there any reason why you can't apply the accordion widget to the inner div here? For example:
<div id="accordion" data-bind="foreach: items">
<h3></h3>
<div><a class="linkField" href="#" data-bind="text: link"></a></div>
</div>
I attempted the accepted solution and it worked. Just had to make a little change since i was getting following error
Uncaught Error: cannot call methods on accordion prior to initialization; attempted to call method 'destroy'
just had to add following and it worked
if(typeof $(element).data("ui-accordion") != "undefined"){
$(element).accordion("destroy").accordion(options);
}
for details please see Knockout accordion bindings break
You could try this to template it, similar to this:
<div id="accordion" data-bind="myAccordion: { },template: { name: 'task-template', foreach: ¨Tasks, afterAdd: function(elem){$(elem).trigger('valueChanged');} }"></div>
<script type="text/html" id="task-template">
<div data-bind="attr: {'id': 'Task' + TaskId}, click: $root.SelectedTask" class="group">
<h3><b><span data-bind="text: TaskId"></span>: <input name="TaskName" data-bind="value: TaskName"/></b></h3>
<p>
<label for="Description" >Description:</label><textarea name="Description" data-bind="value: Description"></textarea>
</p>
</div>
</script>
"Tasks()" is a ko.observableArray with populated with task-s, with attributes
"TaskId", "TaskName","Description", "SelectedTask" declared as ko.observable();
"myAccordion" is a
ko.bindingHandlers.myAccordion = {
init: function (element, valueAccessor) {
var options = valueAccessor();
$(element).accordion(options);
$(element).bind("valueChanged", function () {
ko.bindingHandlers.myAccordion.update(element, valueAccessor);
});
...
}
What I did was, since my data was being loaded from AJAX and I was showing a "Loading" spinner, I attached the accordion to ajaxStop like so:
$(document).ajaxStart(function(){$("#cargando").dialog("open");}).ajaxStop(function(){$("#cargando").dialog("close");$("#acordion").accordion({heightStyle: "content"});});
Worked perfectly.

Resources