Knockout.js how do I build up a string from a binding - javascript

I am creating a list of links using a knockout binding:
The Javascript and view model looks like this:
$(function () {
var adminViewModel = function()
{
var self = this;
self.leftItems = ko.observable([ { Name: "Item1", Id: 0 }]);
self.getChildren = function (id, list) {
var url ="#Url.Content("~/api/Test/GetChildren/")" + id;
$.getJSON(url, function (data) {
list(data);
});
};
}
var Admin3App = window.Admin3App = window.Admin3App || {};
Admin3App.viewModel = new adminViewModel();
ko.applyBindings(Admin3App.viewModel);
function getLeftChildren(id)
{
Admin3App.viewModel.getChildren(id, Admin3App.viewModel.leftItems);
}
getLeftChildren(0);
}
(EDIT: the init of the ko.observable was missing an Id (even though this did not cause an error, added it)
How this works is the view model will load a bunch of items that has no parent (id 0).
For the sake of simplicity I only included the left version. But the page has a list of items on the left and right so there is a function for each.
The left items is populated to view model left items and displayed as below. But each item is a link and once clicked via javascript will refresh the items based on the parent Id. (Much like browsing a folder viewer in explorer).
But i cannot figure out how to declare the binding in knockout to build up the url. I know this is probably rediculously easy and I am just missing it.
here is the html I have attempted amongst others
<div class="leftView">
<div data-bind="foreach: leftItems">
<a data-bind="text: Name, attr : { href:'javascript:getLeftChildren(' + Id + ')' }"></a><br />
</div>
</div>
(EDIT: missed a single quote, but got the same error anyway)
But I keep getting a binding error

A couple of things:
1) leftItems should ideally be an observableArray
2) The initial object you push into leftItems does not have an Id property
Here is a JSfiddle that corrects a few things and demonstrates loading an initial set of data and then calling again to load different data. I've also moved the outside JavaScript call inside the viewModel and switched to using a click handler to pass refresh the data (note that a click binding will automatically send the data item into the event handler as the first parameter, so you have access to the id you need for creating your URL).
http://jsfiddle.net/jearles/xTHFg/

Related

Cannot reinitalize Sortable after ajax content update

I'm using Sortable to organise lists inside of parent groupings, which themselves are also sortable, similar to the multi example on their demo page, but with text. This works fine and uses code along the lines of:
var globObj = {};
function prepSortCats() {
globObj.subCatsGroup = [];
// Changing parent group order
globObj.sortyMainCats = Sortable.create(catscontainer, {
// options here omitted from example
onUpdate: function( /**Event*/ evt) {
// Send order to database. Works fine.
}
});
// Changing sub list order
globObj.subCatsGroup.forEach.call(document.getElementById('catscontainer').getElementsByClassName('subcatlist'), function(el) {
var sortySubCats = Sortable.create(el, {
// options here from example
onUpdate: function( /**Event*/ evt) {
// Send order to database. Works fine.
}
});
});
}
Which is called when the page loads using:
$(document).ready(function () {
// Sortable
prepSortCats();
});
All good so far. However, the user can introduce new elements into any of the lists (sub or parent). In brief, any new elements added by the user are first added to the database, then the relevant section of the page is refreshed using ajax to pull the updated content from the database and display that. The user sees their newly added items added to one of the existing lists. Ajax call is as follows:
function refreshCategories() {
var loadUrl = "page that pulls lists from database and formats it";
$("#catscontainer")
.html(ajax_load)
.load(loadUrl);
return false;
};
This works fine too. Except, Sortable no longer works. I can't drag any lists. My first thought was to destroy the existing Sortable instances and reinitialize them. Right after I called refreshCategories() I call the following:
if(globObj.sortyMainCats.length !== 0) {
globObj.sortyMainCats.destroy();
}
if(globObj.subCatsGroup.length !== 0) {
var i;
for (i = globObj.subCatsGroup.length - 1; i >= 0; i -= 1) {
globObj.subCatsGroup[i].destroy();
globObj.subCatsGroup.splice(i, 1);
}
}
prepSortCats();
But Sortable still has no effect. I introduced the global object (although controversial) so that I could target the Sortable instances outside their scope but I appear to have overlooked something. Any ideas? Apologies for not providing a working example. As I make various ajax calls to a server, I don't think this is possible here.
Update
I'm clearly misunderstanding some action that's taking place. Well, I should preface that by saying I missed that I could still organise the group/parent lists after reloading a section of the page by ajax with refreshCategories(). This is very much a secondary action to being able to sort the sub lists, which is what I noticed was broken and remains so.
But it did point out that although the entirety of $("#catscontainer") was being replaced with a refreshed version of the lists (and that's all it contains, list elements), Sortable still had some sort of instance running on it. I was under the understanding that it was somehow tied to the elements that were removed. Now I'm a bit more lost on how to get Sortable to either: (a) just start from scratch on the page, performing prepSortCats() as if it was a fresh page load and removing any previous Sortable instance, or (b) getting the remaining Sortable instance, after the ajax call to recognise the added elements.
Update 2
Making some progress.
Through trial and error I've found that right after calling refreshCategories(), calling globObj.sortyMainCats.destroy() is preventing even the group lists from being ordered. Then if I call prepSortCats() after this, I can move them again. But not the sub lists.
This isn't conclusive but it looks like I'm successfully destroying and reinitializing Sortable, which was my goal, but something about the ajax loaded elements isn't working with Sortable.
I was looking for the answer in the wrong place, being sure it was an issue with ajax loaded content and the dom having some inconsistencies with what Sortable expected.
Turns out it was an asynchronous problem. Or, to put it simpler, the section of the page being loaded by ajax wasn't quite ready when Sortable was being asked to be reinitalized.
For anyone having the same trouble, I changed:
$("#catscontainer")
.html(ajax_load)
.load(loadUrl);
to
$("#catscontainer")
.html(ajax_load)
.load(loadUrl, function() {
reinitSortable();
});
where reinitSortable() is just a function that fires off the destroy and prepSortCats() functions similar to how they're displayed above.

Adding an event handler inside a knockoutjs custom binding

I'm a fairly experienced knockout user, so I understand quite a bit of the under the hood stuff, I have however been battling now for a few days trying to figure out how to achieve a given scenario.
I have to create a system that allows observable's within a given knockout component to be able to translate themselves to different languages.
to facilitate this, I've created a custom binding, which is applied to a given element in the following way.
<p data-bind="translatedText: {observable: translatedStringFour, translationToken: 'testUiTransFour'}"></p>
This is in turn attached to a property in my knockout component with a simple standard observable
private translatedStringFour: KnockoutObservable<string> = ko.observable<string>("I'm an untranslated string four....");
(YES, I am using typescript for the project, but TS/JS either I can work with.....)
With my custom binding I can still do 'translatedStringFour("foo")' and it will still update in exactly the same way as the normal text binding.
Where storing the translations in the HTML5 localStorage key/value store, and right at the beginning when our app is launched, there is another component that's responsible, for taking a list of translation ID's and requesting the translated strings from our app, based on the users chosen language.
These strings are then stored in localStorage using the translationToken (seen in the binding) as the key.
This means that when the page loads, and our custom bind fires, we can grab the translationToken off the binding, and interrogate localStorage to ask for the value to replace the untranslated string with, the code for our custom binding follows:
ko.bindingHandlers.translatedText = {
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
},
update: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Ask local storage if we have a token by that name
var translatedText = sessionStorage[translationToken];
// Check if our translated text is defined, if it's not then substitute it for a fixed string that will
// be seen in the UI (We should really not change this but this is for dev purposes so we can see whats missing)
if (undefined === translatedText) {
translatedText = "No Translation ID";
}
associatedObservable(translatedText);
ko.utils.setTextContent(element, associatedObservable());
}
}
Now, thus far this works brilliantly, as long as the full cache of translations has been loaded into localStorage, the observables will self translate with the correct strings as needed.
HOWEVER......
Because this translation loader may take more than a few seconds, and the initial page that it's loading on also needs to have some elements translated, the first time the page is loaded it is very possible that the translations the UI is asking for have not yet been loaded into into localStorage, or may be in the process of still loading.
Handling this is not a big deal, I'm performing the load using a promise, so the load takes place, my then clause fires, and I do something like
window.postMessage(...);
or
someElement.dispatchEvent(...);
or even (my favorite)
ko.postbox.publish(...)
The point here is I have no shortage of ways to raise an event/message of some description to notify the page and/or it's components that the translations have finished loading, and you are free to retry requesting them if you so wish.
HERE IN.... Lies my problem.
I need the event/message handler that receives this message to live inside the binding handler, so that the very act of me "binding" using our custom binding, will add the ability for this element to receive this event/message, and be able to retry.
This is not a problem for other pages in the application, because by the time the user has logged in, and all that jazz the translations will have loaded and be safely stored in local storage.
I'm more than happy to use post box (Absolutely awesome job by the way Ryan -- if your reading this.... it's an amazingly useful plugin, and should be built into the core IMHO) but, I intend to wrap this binding in a stand alone class which I'll then just load with requireJs as needed, by those components that need it. I cannot however guarantee that postbox will be loaded before or even at the same instant the binding is loaded.
Every other approach i've tried to get an event listener working in the binding have just gotten ignored, no errors or anything, they just don't fire.
I've tried using the postmessage api, I've tried using a custom event, I've even tried abusing JQuery, and all to no avail.
I've scoured the KO source code, specifically the event binding, and the closest I've come to attaching an event in the init handler is as follows:
init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
// Get our custom binding values
var value = valueAccessor();
var associatedObservable = value.observable;
var translationToken = value.translationToken;
// Set up an event handler that will respond to events on session storage, by doing this
// the custom binding will instantly update when a key matching it's translation ID is loaded into the
// local session store
//ko.utils.registerEventHandler(element, 'storage', (event) => {
// console.log("Storage event");
// console.log(event);
//});
ko.utils.registerEventHandler(element, 'customEvent', (event) => {
console.log("HTML5 custom event recieved in the binding handler.");
console.log(event);
});
},
None of this has worked, so folks of the Knockout community.....
How do I add an event handler inside of a custom binding, that I can then trigger from outside that binding, but without depending on anything other than Knockout core and my binding being loaded.
Shawty
Update (About an hour later)
I wanted to add this part, beacuse it's not 100% clear why Regis's answer solves my problem.
Effectively, I was using exactly the same method, BUT (and this is the crucial part) I was targeting the "element" that came in as part of the binding.
This is my mind was the correct approach, as I wanted the event to stick specifically with the element the binding was applied too, as it was said element that I wanted to re-try it's translation once it knew it had the go-ahead.
However, after looking at Regis's code, and comparing it to mine, I noticed he was attaching his event handlers to the "Window" object, and not the "Element".
Following up on this, I too changed my code to use the window object, and everything I'd been attempting started to work.
More's the point, the element specific targeting works too, so I get the actual event, on the actual element, in the actual binding that needs to re-try it's translation.
[EDIT: trying to better answer the question]
I don't really get the whole point of the question, since I don't see how sessionStorage load can be asynchronous.
I supposed therefore sessionStorage is populated from som asynchronous functions like an ajax call to a translation API.
But I don't see what blocks you here, since you already have all the code in your question:
var sessionStorageMock = { // mandatory to mock in code snippets: initially empty
};
var counter = 0;
var attemptTranslation = function() {
setInterval(function() { // let's say it performs some AJAX calls which result is cached in the sessionStorage
var token = "token"; // that should be a collection
sessionStorageMock[token] = "after translation " + (counter++); // we're done, notifying event handlers
window.dispatchEvent(new Event("translation-" + token));
}, 500);
};
ko.bindingHandlers.translated = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var val = valueAccessor();
var token = val.token;
console.log("init");
window.addEventListener("translation-" + token, function() {
if (token && sessionStorageMock[token]) {
val.observable(sessionStorageMock[token]);
}
});
}
};
var vm = function() {
this.aftertranslation = ko.observable("before translation");
};
ko.applyBindings(new vm());
attemptTranslation();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="translated: { observable: aftertranslation, token: 'token' }, text: aftertranslation" />

how to add partial page functionality using $http service in angular?

I want to perform operation after successfully loaded the partial page using $http service in angular.
The operation is to check the checkbox based on the scope value.
Please anyone help me.
Source Code here:
This is actual code.
$http({
url : './resources/staticPages/custom-object-filter.html',
method : "GET"
}).success(function(data, status) {
$scope.data = data;
jQuery("objectViewPartial").html($compile($scope.data)($scope));
//console.log($scope.selected);
if(angular.equals($scope.selected, "ShowActivatedObjects")) {
$("#ShowActivatedObjects").attr('checked','true');
} else {
$("#ShowActivatedObjects").attr('checked','false');
}
}).error(function(data, status) {
console.log("some error occured partial page");
});
After getting success the below code is not working.
if(angular.equals($scope.selected, "ShowActivatedObjects")) {
$("#ShowActivatedObjects").attr('checked','true');
} else {
$("#ShowActivatedObjects").attr('checked','false');
}
I placed this code inside of success function.
Please advice where i need to place this code.
A short Angularjs excursus:
In general you should minimize the use of jQuery within angularjs as far as you can, because both concepts are working against each other.
Angularjs is based on bindings between scope variables and the template expressions. So for a check box you can easy handle it without jQuery as follows:
A good explanation you can find here:
https://stackoverflow.com/a/14520103/4852206
What you are seeing here is, that you i.ex. can bind the checkbox inputs to an array. If you are changing the array content, the checkboxes are listening and do change there state (checked or unchecked) just on the fly without the need of checking if it is active and without the Jquery dom manipulation needs.
I wrote the above excursus, because you will run into huge unstructured and unreadable code if you will use your way as best practice. Also I am afraid, that you are using your code within your controller function. In Angularjs the controller is NOT for dom manipulation, you should only use it for setting / changing the scope variables or just a minimal business logic (in general avoid it here!) Business logic should go into services and dom manipulation should go into directive's.
Your question:
Please provide more information for your problem / logic. Please provide sample checkboxes, and sample information that you pull from the server via $http
Please provide also what you want to achieve exactly. Then we can help you with a clear answer.
Sorry I know this is no answer, but I cannot add comments actually cause I am quite new here.
EDIT
Ok thanks to your comment to #Likeee I think I got you. You have a set of checkboxes on your page. On page load you want to check which checkbox needs to be active. The logic which checkbox is active on pageload comes from the server side, is that correct? If yes I would handle it as follows with angular structure (I created a fiddle for a better understanding: https://jsfiddle.net/pfk9u3h0/4/)
First you need your HTML:
<body ng-app="myApp" ng-controller="AppController">
<label ng-repeat="filter in productFilter">
<!--
simply checking if the array contains the entry. if not, it is not selected. On a click event, the selected item is added to the array.
-->
<input
type="checkbox"
value="{{filter}}"
ng-checked="selection.indexOf(filter) > -1"
ng-click="toggleSelection(filter)"/> {{filter}}
</label>
"selection Array within SelectionService": <br />{{selection}} <br /><br />
as you see, the array is also updating when you check or uncheck a box
</body>
This HTML snipped does several things:
First it declares which controller (here "AppController" to use.
Second it creates a label with a checkbox inside. The label has the Angularjs directive ngRepeat and causes to show as much checkboxes as the array "productFilter" within AppController contains.
The Javascript contains a controller which is talking to your HTML and to a service. The service is also declared below.
Controller:
app.controller("AppController", function( $scope, SelectionService ) {
// available filter checkboxes
$scope.productFilter = ['Red pants', 'Green Pants', 'Yellow pants', 'Blue pants'];
// selected filter
// normally you just would have this: $scope.selection = ['Red pants', 'Blue pants'];
// But now you want to pull the selection elsewhere:
// The selection should go in a service, because it can happen, that other pageviews of your page shall
// change the values or even get the selection as well.
$scope.selection = SelectionService.selection; // Bind the selection to the service
// With the proper binding, you can change the selection within other modules / pages etc. and this
// checkbox fields will update based on the service data!
//I just initialize the service selection array, butbut you really want to pull it from server like this:
// SelectionService.loadSelection()
// .then(function(data) {
// if you like, yo can do sth. with this promise callback
// });
// Change selection for the clicked input field
$scope.toggleSelection = function toggleSelection(filter) {
var idx = $scope.selection.indexOf(filter);
// is currently selected
if (idx > -1) {
SelectionService.removeSelection(idx);
}
// is newly selected
else {
SelectionService.addSelection(filter);
}
};
});
With $scope.productFilter = ['Red pants', 'Green Pants', 'Yellow pants', 'Blue pants']; we declare an array over which we iterate within our HTML. This contains all checkboxes with names.
Now we need an array with all selected checkboxes. This is saved in $scope.selection and is bound to the service which i show at the end:
$scope.selection = SelectionService.selection;
The rest within the controller is just to set the new values, if you uncheck or check a checkbox
The Service:
app.service("SelectionService", function ($http, $q) {
var obj = {
selection : ['Red pants', 'Blue pants'],
loadSelection : function(){
var that = this;
var deferred = $q.defer();
var config = {
responseType : "json",
cache: true
}
$http.get("/getSelectionFromServer", null, config)
.success(function(response){
if(response) {
// IMPORTANT: Use a deep copy, otherwise you will loose the binding
angular.copy(response,that.selection);
}
deferred.resolve(response);
})
.error(function(){
deferred.reject();
});
return deferred.promise;
},
addSelection : function (filter) {
this.selection.push(filter)
},
removeSelection : function (index) {
this.selection.splice(index, 1);
};
return obj;
});
The Service is doing 4 things: It holds and initilizes (if you like) an array "selection". And it offers a method to load new data from server and save it to the array. Here it is important that you use the copy method, otherwise you will loose any binding. And it offers methods to set and remove selections.
in my example i dont use the load method, because I do not have any serverside scripts here...I just take the initialization values. But you should use my load method.
Hi your comparison is probably bad.
if(angular.equals($scope.selected, "ShowActivatedObjects")) {
I don't know what value is in $scope.selected, but i think this value is a checkbox state which is boolean type. So you are comparing boolean with string.
Check out https://docs.angularjs.org/api/ng/function/angular.equals
Finally i achieved my requirement:
// $scope.getCustomObjectsBasedOnObjectTypeSelection = getCustomObjectsBasedOnObjectTypeSelection;
$scope.selected = ['Show All'];
var IsActive = "";
$scope.objectSelection = function objectSelection(objectType) {
var idx = $scope.selected.indexOf(objectType);
// is currently selected
if (idx > -1) {
$scope.selected.splice(idx, 1);
}
// is newly selected
else {
$scope.selected.push(objectType);
}
};

Ensure knockout binding gets processed first

I have a table created from an observable array. Table rows contains elements belonging each to one of a set of categories. To filter the table based on categories, there is a row of buttons.
Buttons can be active or inactive, indicated via a CSS class bound via knockout:
<button data-bind="click: filter.filterCategory, css: { filterOn: filter.category.isFiltered() }">Filter</button>
Filtering within the table is done by switching the display state of rows:
<tr data-bind="css: { nonDisplay: !table.category.isDisplayed() }">
</tr>
The click handler mainly sets the values of the two observables, in sequence e.g.
vm.filter.category.isFiltered(true);
vm.table.category.isDisplayed(false);
This works in principle.
The problem is that the indication that the filter button has been selected by the user is not given immediately, but dependent on the execution time of the filtering itself, i.e. the changes to the table.
With larger tables, and especially on mobile, this can mean delays of a couple of seconds.
I can live with the filtering itself taking this long, but the feedback needs to be immediate.
Is there a way to ensure that the change on vm.filter.category.isFiltered gets applied before the longer running change based on vm.table.category.isDisplayed is started?
This seems as an async fail.
You should implement a callback method parameter in the isFiltered method, something like this
var vm = vm || {};
vm.filter = vm.filter || {};
vm.filter.category = (function($){
var that = this;
that.isFiltered = function(booleanValue, callback) {
// Put your primary code here
console.log("this runs first");
// ...when the first code is done
callback();
};
that.isDisplayed = function(booleanValue) {
console.log("this runs second");
};
return that;
})($);
// Implement by stating a method as the second parameter.
vm.filter.category.isFiltered(true, function(){ vm.filter.category.isDisplayed(false); });
This will render
// this runs first
// this runs second

Dynamically Binding Items in ListVIew

In my Windows 8 JavaScript application, I have a ListView. I need to add either a message or link to a row, depending on what the current user's status is. So I essentially need to show or hide items depending on some flag. How, using the JavaScript API of the ListView, do I parse items at an item level? There is no collection of items on the ListView control per the MSDN, and I need to have access to the data and the item at the row level.
I'm sure I'm missing it somehow, just getting into this....
I'm not completely clear on what your trying to do, but I'll give it a shot.
If you need to conditionally show or hide items (or certain parts of an item) there and a couple of ways you can go.
The first is to create an imperative template render function. First, tell you ListView that its item template is a function with something like myListView.itemTemplate = myCustomFunction. Then write a function like:
function myCustomFunction(itemPromise) {
//you have to return a promise
return itemPromise.then(function (item) {
//get the right item template (declared in your HTML),
//render the item data into it, and return the result
var itemTemplate;
if (item.data.key === "foo")
itemTemplate = q("#fooItemTemplate", element); //return foo template
else if (item.data.key === "bar")
itemTemplate = q("#barItemTemplate", element);
return itemTemplate.winControl.render(item.data);
});
}
If one of the item templates has explicit style code to show or hide part and the other doesn't then you'll get the result you're looking for.
Another way is to wait until the ListView is finished loading and then traverse and manipulate the DOM according to your conditions.
To capture the moment your ListView finishes loading do this:
myListViewControl.onloadingstatechanged = function (e) {
if (myListViewControl.loadingState == "complete") {
...
}
}
The ListView goes through a few loading states as it's loading and the last is "complete". When it's complete, you can use the awesome power of CSS selectors and the new querySelector/querySelectorAll method that ECMAScript 5 gives us to find all of the ListView items like this:
myListView.querySelectorAll(".win-item")
That would return a NodeList of all of the ListView items and you can use your ninja skills in DOM manipulation to have your way with them.
Hope that helps!!
P.S. Check out codeSHOW to learn more HTML/JS dev in Windows 8 (aka.ms/codeshowapp | codeshow.codeplex.com)

Categories

Resources