I have an AJAX MVC Contrib Grid implementation, that already existed and now I am in a situation where I am trying to bolt on some knockout functionality... and I want to know if this is possible without changing the whole grid implementation.
This is the refresh grid function that is setting the container html when the pagination changes.
scope.refreshGrid = function (container, url) {
if (url)
container.data(scope.selectors.actionUrlAttribute, url);
$.post((url || container.data(scope.selectors.actionUrlAttribute)), scope.getParams(),
function(html) {
container.html($(html).html());
scope.bindDeleteButtons();
}).done(function() {
container.trigger("refresh.ctb.grid");
});
}
one of the columns for the grid is custom column that uses Html.Partial like this:
column.Custom(x => Html.Partial("_CartSelection", new CartSelection(x.Id)));
The partial view has the below markup with some knockout data bindings
<input type="checkbox" value="#Model.Id" data-bind="enable: (selectionEnabled() || $element.checked), checked: selectionIds" />
This works for the first page of results, when the paging is selected to change the page and the container html() is updated the bindings no longer work but the KO viewModel still has the correct selectionIds.. which is what I was expecting to happen.
The KO view model is being applied as shown below, where the grid has a wrapper parent div with an id of "cart":
$(function() {
var viewModel = new IP.Configuration.CartSelector(new IP.Router());
ko.applyBindings(viewModel, document.getElementById("cart"));
});
I have already seen comments in other posts about how you shouldn't re-apply bindings. In my case it seems I want to apply bindings but only to some child nodes that are being dynamically loaded.
Is this possible?
UPDATE:
Almost had this working by adding a cart-selection class to each checkbox and doing the below in a rebind function on the viewModel, where self is the viewModel:
$("#cart .cart-selection").each(function(index, item) {
ko.applyBindings(self, item);
});
Then doing the below on the custom trigger for refreshing the grid, when the content is reloaded.
$("#cartGrid").on("refresh.ctb.grid", function() {
viewModel.rebind();
});
The issue I am finding with this at the moment is that the checkboxes are no longer enabled regardless of the $element.checked binding.. maybe a valueHasMutated will fix this, still looking into this.
I figured out what my remaining problem was, it was due to the ordering of the data bindings.
The enable data bind needed to be placed after the checked binding since it has a dependency on it via $element.checked which makes sense now after realising it!!
I changed my rebind function slightly to the below:
var gridResult = $("#cartGrid table");
if (gridResult.length > 0)
ko.applyBindings(this, gridResult[0]);
Each refresh brings in a new table but at least now if I add any more bindings to other elements in the results from the grid, they will work as expected.
Related
I am using Knockout-Kendo.js to bind Kendo widgets to Knockout observables. I have a KendoListView that populates itself from html template based on a observable named "Filters". The problem I am having is that the input control that I am using inside the template is not converting into a Kendo widget, even though I am specifying it as a kendoComboBox. (I have left out properties of dataSource, etc for simplicity)
Other things to take note of.
On page load, there are no objects in the Filters observable property
in the Model.
ko.ApplyBindings(Model) is called within the Document ready function.
Users make various selections on the page, which then populates the
Filter observable in the model.
The controls do show up in DOM when Filters are added, but as native Html controls.
var Model = {
Filters: ko.observable([]),
FilterItemTemplate: function () {
return kendo.template($("#FilterItemTemplate").html())
}
}
<div id="Filters" data-bind="kendoListView: { data: Filters, template: FilterItemTemplate()}" ></div>
<script type="text/html" id="FilterItemTemplate">
<div>
<h4>#=ControlLabel#</h4>
<input id="#=ControlID#" name="FilterControl" data-bind="kendoComboBox: {}" />
</div>
</script>
What I wind up doing is
Removed the ListView from the Filters div.
Since the ListView is now removed, i subscribed to the Filters observable in my model with my javascript code. So whenever something is loaded or removed from the Filters observable, the function listener will be invoked.
Within the listener function manually created the Kendo widgets and append to the Filters div.
My page fires off an ajax query, where the MySQL Db is queried and the results are returned. (all successful).
Those results are formatted for output as a shopping gallery/catalogue and also as an accordion filter menu. So I can filter the shopping catalogue display. eg say I want to see only items that are red.
All is working so far.
My problem is with the filter accordion menu - dynamically created in js.
When I click on any selectable item in the tab-content, nothing happens. This means the parameter that should be sent, isn't being sent.
If I hard code the accordion filter or even load it with my server-side language, into the html directly, the filtering does send off the parameter and so the shopping catalogue is adjusted accordingly but, in that scenario, I am unable to dynamically change the filter menu.
I think the code I shall post below is the relevant code that recognises changes in the originally loaded content and fires off the ajax but (I think) it doesn't understand any changes to textboxes in the dynamically loaded content.
Please help me to understand what I need to add that will make dynamically loaded content fire-off to the ajax calls.
var $checkboxes = $("input:checkbox");
function update_nav_filter(opts) {
$.ajax({
type: "POST",
url: "/php-queries/product-filter-query.php",
dataType: 'json',
cache: false,
data: {
filterOpts: opts
},
success: function(records) {
//console.log(records);
//alert('SUCCESS!');
// alert(records);
$('#filters_div').html(makeFilter(records));
}
});
}
$checkboxes.on("change", function() {
//alert('there is a change is checkbox status'); // working on page load but not when any checkbox is clicked-on
var opts = getCatalogueFilterOptions();
updateCatalogue(opts);
update_nav_filter(opts);
});
$checkboxes.trigger("change");
Any help greatly appreciated.
I have created an event listener.
Following page-load, I select an item in the JS generated nav filter. eg pedal_bins in the sub_category section. I am then shown a display of pedal_bins. :)
Then I select 'kettles', another sub_category but I can only see the last sub_category that I click on. The pedal_bins disappear.
How best can I build and remove items with a single click? Store in a session parameter and then
a. remove the latest click if it matches whats in the session
b. add the latest click if its not already in the session
Then submit whatever the array is at that stage?
Or, is there a better way to run this?
Here's the listeneer
enter code here
document.getElementById("filtering_div").addEventListener("click",function(e) {
// e.target was the clicked element
if (e.target && e.target.matches("input")) {
var parameter = e.target.id;
//console.log("Anchor element", parameter , " was clicked" );
var opts = getCatalogueFilterOptions(parameter);
console.log(opts);
// update_nav_filter(opts);
updateCatalogue(opts);
}
});
You have a "delegation" problem. When you create a dynamic element, in order to be able to act on the newly created element, you have to reference it as a child element that was originally loaded with the DOM.
For example, if you have an element called <div id="top"></div> and you create a dynamic element, let's say <button id="test">Click</button> in there, you'll have to refer to that div when adding an event listener.
$("#top").on('click', '#test', function(){
//event related code goes here.
});
Here is a fiddle I created that explains the whole thing with some examples.
If you have any questions about it, please let me know.
I am trying to initialize a dropdown widget using Foundation. The actual html for the dropdown is compiled dynamically, and multiple dropdowns exist on the page. I am having trouble initializing the dropdowns because of their dynamic nature.
Essentially my question is, how do you initialize a Foundation dropdown without an ID or data-dropdown attribute?
I have a screen with a list of data "People" on the left, and a the details of a single person on the right. The whole screen is using Knockout to bind the data to the view model.
My main View model:
self.people = ko.observableArray([]);
self.selectedPerson = ko.observable()
In my html:
<div data-bind='with: selectedPerson'>
...
<div class='my-dropdown-widget' data-bind="myCustomBinding: {data: myData}">
...
</div>
</div>
Each selectedPerson is a Person object, which has an onReady function that fires when the data from the person is rendered in the main view model. In that function, I have the following code:
self.onReady = function (el) {
var filterEl = $('.filter-widget', el).attr({
'id': self.filterWidget.ui_ID,
'data-dropdown': ''
});
setTimeout(function () {
$(el).foundation();
}, 500);
self.ui_container = $(el);
};
I have to have the dropdown widget on the page (as opposed to loading the HTML for it) so i can apply bindings to it. Applying Bindings on dynamically loaded html messes up the widget's binding context.
This approach fails with the following message:
Uncaught TypeError: Cannot read property 'className' of undefined
This error usually occurs because the element does not have a data-dropdown attribute, which I am adding dynamically.
If I do this the way that is illustrated in the Foundation Docs, and add an ID and data-dropdown attribute to the dropdown element, the dropdown works, but only for the first selectedPerson. In other words, if I navigate to more than one person, the dropdown stops working properly.
How can I get these dropdowns to work given the setup of my page?
I'm attempting to rebind the listview data after changing the template, based on a DropDownList value. I've included a JSFiddle for reference. When I rebind currently the values in the template are undefined.
Thanks!
JSFiddle link
I was thinking the best way to handle it would be in the 'select' or 'change' function:
var cboDetailsCategory = $("#detail").kendoDropDownList({
data: [
"All",
"Customer",
"Location",
"Meter",
"Other"],
select: function (e) {
var template = $("#" + e.item.text()).html();
console.log("template", template);
$("#details").html(template);
},
change: function (e) {
},
please refer to the JSFiddle link and this graphic as a visual
Here is a lengthier workflow:
User completes a name search and clicks a search button.
Name results are populated in a listview, rendered individually as button controls using a template.
User then clicks one of the name results (shown as the button text).
A dropdownlist of categories ('All' <--default , 'Location', 'Customer'...) gives the user the ability to target what subject of data they want to see. 'All' is the default, showing all details about the selected name.
So by default the 'All' template is populated.
If user wants to see the 'Location' details (template) they select it from the dropdownlist.
The template shows but the values are all blank. The only way to populate it is to click the name (button) again.
I want to remove the need for having to re-click the button (name) to populate the template ('Location', etc...).
I have put together a JSFiddle showing the structure. Though due to the data being private and served over secure network I cannot access it.
Refer to JSFiddle:
I believe the issue is that the onclick event grabs the data-uid and passes it to the initial default template (named 'All' but it's not included in code as it's lengthy). When the user changes the dropdownlist (cboDetailsCategory) and selects a new template I lose the data.
Thanks for your help. I'm really stuck on this and it's a current show stopper.
There isn't an officially supported way to change templates, without destroying the listview and rebuilding it. However, if you don't mind poking into into some private api stuff (be warned I can't guarantee that kendo won't break it without telling you) you can do this
var listview = $("#MyListview").getKendoListView();
listview.options.template = templateString;
listview.template = kendo.template(listview.options.template);
//you can change the listview.altTemplate the same way
listview.refresh(); //redraws the elements
if you want to protect against unknown API changes you can do this, which has A LOT more overhead, but no risk of uninformed change (untested!)
var listview = $("#MyListview").getKendoListView(),
options = listview.options;
options.dataSource = listview.dataSource;
listview.destroy();
$("#MyListview").kendoListView(options);
Here's the solution, thanks for everyone's help!
JSFiddle Link
The issue was where I was setting the bind:
$("#list").on("click", ".k-button", function (e) {
var uid = $(e.target).data("uid");
var item = dataSource.getByUid(uid);
var details = dropdown.value();
var template = $("#" + details).html();
$("#details").html(template);
kendo.bind($("#details"), item);
currentData = item;
});
See edit at the bottom.
My company has a huge code base and we want to start using knockout more effectively. However, we have validation code in place already that takes care of all aspects of client-side validation. It uses jQuery to show validation error messages and to sanitize user input.
For example, if I add the class "validate-range" to an input, it will use jQuery change/focusout events to track changes and then if a value is out of the range, it will replace it with the min/max value using $(input).val(). Since this validation code makes changes this way programmatically, my knockout view model won't be updated when these kind of changes are made.
This validation code is used everywhere in the system, and can't be replaced at the moment, so in order to use knockout, I have to make it work along side this code. What i've tried so far is creating a custom value binding that adds an additional change event handler which is used to update the view model whenever the validation code changes an input's value.
This works surprisingly well in all cases except inside a foreach binding (which is the same as using the template/with binding I would imagine). My change event handler isn't being fired on any inputs inside the foreach that use the custom value binding, even though the custom binding is being reapplied to all inputs inside the foreach every time the observable array changes.
I was hoping someone has dealt with this problem before, having to make knockout work with existing javascript code that changes DOM values, and thus doesn't update the view model. Any help is greatly appreciated.
Javascript code for custom binding, creating view model, and old validation code:
// custom value binding for amounts
ko.bindingHandlers.amountValue = {
init: function (element, valueAccessor) {
var underlyingObservable = valueAccessor(),
interceptor = ko.computed({
read: function () {
var value = underlyingObservable();
return formatAmount(value);
},
write: function (newValue) {
var current = underlyingObservable(),
valueToWrite = parseAmount(newValue);
if (valueToWrite !== current)
underlyingObservable(valueToWrite);
else if (newValue !== current.toString())
underlyingObservable.valueHasMutated();
}
});
// i apply a change event handler when applying the bindings which calls the write function of the interceptor.
// the intention is to have the change handler be called anytime the old validation code changes an input box's value via
// $(input).val("new value"); In the case of the foreach binding, whenever the observable array changes, and the table rows
// are re-rendered, this code does get ran when re-applying the bindings, however the change handler doesn't get called when values are changed.
ko.applyBindingsToNode(element, { value: interceptor, event: { change: function () { interceptor($(element).val()); } } });
}
};
// view model creation
// auto create ko view model from json sent from server
$(function () {
viewModel = ko.mapping.fromJS(jsonModel);
ko.applyBindings(viewModel);
});
// old validation code
$(document).on("focusout", ".validate-range", function () {
var $element = $(this),
val = $element.val(),
min = $element.attr("data-val-range-min"),
max = $element.attr("data-val-range-max");
if (val < min)
// my change handler from custom binding doesn't fire after this to update view model
$element.val(min);
if (val > max)
// my change handler from custom binding doesn't fire after this to update view model
$element.val(max);
// more code to show error message
});
HTML code that uses the custom binding inside of a foreach binding:
<table>
<thead>
<tr>
<td>Payment Amount</td>
</tr>
</thead>
<tbody data-bind="foreach: Payments">
<tr>
<td><input type="text" class="validate-range" data-val-range-min="0" data-val-range-max="9999999" data-bind="amountValue: Amount" /></td>
</tr>
</tbody>
</table>
So in the above example, if I enter "-155" in an amount text box, my custom binding runs and sets the view model Amount to -155. Then the old validation runs and re-sets the value of the textbox to "0" with $(input).val(0). My view model doesn't get updated at this point, and still reflects the -155 value. My change event handler from the custom binding is supposed to be ran to update the view model to 0, but it doesn't.
Edit:
As pointed out in the answer, .val() does not trigger any change events. The change event handler I added didn't do anything. The reason the view model was being updated when the validation code changed a value outside of the foreach binding was because we had logic somewhere else in our javascript code that was manually triggering the change event using the blur event, which in turn triggered my custom binding to run and update the view model. This blur event handler was directly bound to the text boxes, instead of being delegated, so it worked for text boxes that were there when the page is first rendered, but not for the ones dynamically inserted by the foreach binding.
For now, I just changed this logic to delegate the events within the document, so it would include dynamically inserted text boxes, and it seems to be working fine. I'm hoping to come up with a better solution in the future.
Calling $(element).val("some value"); does not trigger the change event.
You would need to do: $(element).val("some value").change();