So I have two viewModels, one has a document style database in an observable:
var databaseViewModel = function () {
var self = this;
self.database = ko.observableArray([]).publishesTo("database");
}
var calcViewModel = function () {
var self = this;
self.replicatedDatabase = ko.observableArray([]).subscribeTo("database");
}
These get applied thusly:
ko.applyBindings({
databaseViewModel: new databaseViewModel(),
calcViewModel: new calcViewModel()
});
The only problem is, that the drop down box tied to replicatedDatabase doesn't show any items. I know I can force a binding update:
database.valueHasMutated();
But I don't know where and when.
I have tried after the ko.applyBindings however, I'm not sure how to access the newly created databaseViewModel. I've tried inside databaseViewModel after it has been created, but I believe that KO automatically updates the bindings when they've been binded, so not sure this actually makes a difference, it didnt on the dropdowns anyways.
I'm not really sure what I should be doing here.
For reference, I'm using knockout-postbox to do message bus style communications, subscribeTo and publishesTo. So as soon as the database observable is changed it will notify all subscribers, so I thought that maybe replicatedDatabase would have been update in the instance that databaseViewModel was initiated.
So, rather than force knockout to update the values I chose a different approach.
Realistically speaking the page would initially be populated with some data from a server, so with this in mind I proceeded by making a global variable holding the initial data:
var serverData = [{}];
Then just simply populate the observableArray's using Ryan Niemeyer mapping function:
ko.observableArray.fn.map = function ( data, Constructor) {
var mapped = ko.utils.arrayMap(data, function (item) {
return new Constructor(item);
});
this(mapped);
return this;
};
This way both viewModel's start off with the initial data, and when the database viewModel gets updated this permeates through to the other viewModel's
Related
I've been trying to understand async, promises, etc. and I think I have a basic understanding of it, but I'm not getting the results I expect.
I have a HTML table, with the following:
<table data-bind="visible: viewPrincipal()">
viewPrincipal() is a function that should return true or false. This does work at the most basic level if viewPrincipal() just consists of return false or return true. But what I'm trying to do is call an async function to get the true or false value from there.
function viewPrincipal() {
console.log("Seeing if person is in principal group");
return IsCurrentUserMemberOfGroup("Principal Members", function (isCurrentUserInGroup) {
console.log(isCurrentUserInGroup);
return isCurrentUserInGroup;
});
}
The console.log works, and returns a true or false as I'd expect it to. But I want the parent viewPrincipal() function to return that true or false value, and all I get is "undefined".
I understand why this is happening - the IsCurrentUserMemberOfGroup() function is taking a bit of time to complete - but I don't know how to fix it. I know how to chain functions together, but when I'm trying to use something like knockout.js to determine if a table should be visible or not, I don't know how to chain.
Can anyone help?
The best way is to use an observable bool, and let your a-sync function change it's value. Let the magic of two-way-bindings do the rest.
Example:JSFIDDLE
function vm() {
this.viewPrincipal = ko.observable(false);
};
var vm = new vm();
ko.applyBindings(vm);
function fakeAsync() {
setTimeout(() => {
vm.viewPrincipal(true);
}, 1500);
}
fakeAsync();
I am a bit lost with your approach, but I'll try to help.
First, please double-think whether you really want to implement access control on the client side. Simply hiding an element if the user does not have sufficient rights is pretty dangerous, since the (possibly) sensitive content is still there in the DOM, it is still downloaded, all you do like this is not displaying it. Even a newbie hacker would find a way to display it though - if nothing else he can simply view it using the F12 tools.
Second, is that triple embedding of functions really necessary? You have an outermost function, that calls a function, which, in turn, calls the provided callback. You could clear this up by using computed observables:
function viewModel() {
var self = this;
var serverData = ko.observable(null);
this.viewPrincipal = ko.computed(function() {
var srvDataUnwrapped = serverData(); // access the inner value
if (!srvDataUnwrapped) {
return false;
}
// Do your decision logic here...
// return false by default
return false;
});
// Load the permission details from the server, this will set
// a variable that the viewPrincipal depends on, this will allow
// Knockout to use its dependency tracking magic and listen for changes.
(function() {
$.ajax(url, {
// other config
success: function (data) {
serverData(data);
}
);
})();
};
var vm = new viewModel();
and then in your view:
<table data-bind="visible: viewPrincipal">
note the lack if ()'s here, it is an observable, so Knockout will know how to use it.
If this seems overly complicated to add to your already existing code, then you could simply define an observable instead, and set the value of that inside your callback:
function viewModel() {
// other stuff ...
this.viewPrincipal = ko.observable(false);
// Call this wherever it fits your requirements, perhaps in an init function.
function checkPrincipal() {
IsCurrentUserMemberOfGroup("Principal Members", function (isCurrentUserInGroup) {
viewPrincipal(isCurrentUserInGroup);
});
};
};
With this approach, the markup would be the same as in the previous one, that is, without the parentheses:
<table data-bind="visible: viewPrincipal">
Doing it this way will simply set the inner value of an observable inside the callback you pass to IsCurrentUserMemberOfGroup, and because Knockout is able to track changes of observables, the value change will be reflected in the UI.
Hope that helps.
I am trying to get songs from soundcloud, I am using some input to set value and send it to my factory to get all the related list of songs and display it.
The issue is the the first time all works correctly, but when I am trying to input new values I am getting same results as first time.
My code looks like:
.controller('DashCtrl', function ($scope, SongsService) {
$scope.formData = {};
$scope.searchSong = function () {
SongsService.setData($scope.formData.songName);
};
UPDATE
the factory :
.factory('SongsService', function ($rootScope) {
var List = {};
List.setData = function (tracks) {
var page_size = 6;
SC.get('/tracks', {limit: page_size, linked_partitioning: 1}, function (tracks) {
// page through results, 100 at a time
List = tracks;
$rootScope.$broadcast('event:ItemsReceived');
});
};
List.getItems = function () {
return List;
};
return List;
}).value('version', '0.1');
Thanks for help!
It's hard to tell without a plunkr reproducing the issue and showing all your relevant code, but I think your problem is that you're overwriting the List variable in the async answer, and this List (I assume) is the object you originally returned from your factory.
I see two noteworthy concepts here:
the fact that angular factories are effectively singletons
and that javascript objects are passed by reference-by-value (see call-by-sharing, or one of many stackoverflow discussions).
An angular factory is a singleton, meaning the factory function will only be called once, before the first injection, and every controller it's injected into will work with the same object reference it returned. If you overwrite this object reference, well, the previous value (which the controller has) is still a reference to the original object.
Edit: In fact, by overwriting List you're creating a new object which doesn't even have a setData method anymore!
You probably want to make List private to SongsService, and return a more complex object from the factory that captures List in a closure, and offers some public getter/setter methods for it. (If you insist on replacing the contents of the returned List variable, empty the object and extend it with the new properties - including this method again. But this is much more work, and not a nice solution.)
In Angular Service constructors and Factory methods are singleton objects. You need to return a method that you can call. Your code examples are incomplete so it is hard to tell what is going on. What is returned by your factory method, the List object?
If so, when the first call is completed, it overwrites the List object so that the setData method can't be called a second time. What is the SC object, I can not see in your example how you are injecting it. You probably want to fix that too.
Consider this possible solution.
Service
Songs.$inject = ['$http'];
function Songs($http) {
this.$http = $http;
}
Songs.prototype.getSongs = function(searchTerm) {
return this.$http.get('http://someendpoint/songs', {searchTerm: searchTerm});
}
service('songs', Songs);
Controller
DashController.$inect = ['songs'];
functionDashController(songs) {
this.songs = songs;
this.results = [];
}
DashController.prototype.searchSongs = function(searchTerm) {
var self = this;
this.songs.getSongs(searchTerm).then(function(results) {
this.results = results;
});
}
controller('DashController', DashController);
This is example uses the best practice controllerAs syntax explained here: http://toddmotto.com/digging-into-angulars-controller-as-syntax/
I found the issue,
I got same results all the time because I didnt use cooreclty the api of soundcloud, I didnt send the title on the api... also you are correct, I should not set the list as empty..I should set some value to the list...
I have an array of "users" in a KnockoutJS observable array. I can add users to the array and it updates the view correctly, but the editing of users is currently driving me nuts, probably because of a slight lack of understanding of the base concept on my part.
View Model
var viewModel = {
users: ko.observableArray(),
user: ko.observable(),
showEditForm: function (model) {
if (!$('#users-form').is(':visible')) {
$('#mask').show();
}
showUsersLoading();
loadUserIntoEditForm(model.Id);
},
getUser: function (userId) {
for(var i = 0; i < this.users().length; ++i)
{
if (this.users()[i].Id === userId)
{
this.user(this.users()[i]);
break;
}
}
}
};
User View Model (this is primarily used for the add functionality at the moment)
var userViewModel = function (id, username, statusDescription, email) {
var self = this;
self.Id = ko.observable(id),
self.Name = ko.observable(username),
self.StatusDescription = ko.observable(statusDescription),
self.Email = ko.observable(email)
};
The updating / editing is performed in an MVC partial view that fires off an ajax request to update the user server side, then on a successful response, runs the following code to update the user
viewModel.getUser(result.Id);
viewModel.user().StatusDescription('locked');
viewModel.user().Name('testingUpdate');
Which gives me a Uncaught TypeError: string is not a function error
This is where my understanding fails me. I get that the user that I've grabbed from the users array doesn't have observable properties, which is why I can't update using the Knockout function method, I've confirmed this by pulling out details of the users array in the browser console window.
I also know that, conceptually, I want to cast the user observable object to a userViewModel object so the properties then become observable and I can update them; or I want the users observable array to know that the objects it contains should be of type userViewModel, so the object properties are observable.
The problem I'm having is although I understand the concept, I can't figure out the code to actually make it work.
The problem is the this keyword. In your sample it's not referring to what you expect.
Try using the RMP (revealing module pattern) to simplify the way you write your code. It looks something like this:
var viewModel = (function() {
var users = ko.observableArray();
var user = ko.observable();
// This one changes: you can use the vars directly
var getUser = function (userId) {
for(var i = 0; i < users().length; ++i)
{
if (users()[i].Id === userId)
{
user(this.users());
break;
}
}
}
// Reveal the data
return {
users: users,
user: user,
getUser : getUser
};
})(); // self-executing anonymous function
The anonymous function puts a closure around your vars. You can use the vars safely inside that closure, and they are not available outside of it. Besides you can have "private vars" by simply don't revealing them.
Additional note: I recommend you to use lodash or underscore to simplify array manipulation: you'd avoid writing loops to find an element in an array. For example using lodash find.
As always, the easy answer is, I'm an idiot.
The key part that I was missing is forgetting that I'm not dealing with strongly typed objects, and assuming that they'll just work the way I expect.
The success callback for my ajax call to populate the users() array in the viewModel object used to be
success: function (data) {
viewModel.users(data)
setupDatatable();
}
Which, looking back at it, the problem is obvious. I'm inserting generic objects into the array, not userViewModel objects.
So, when I changed it to:
success: function (data) {
for (var i = 0; i < data.length; ++i) {
viewModel.users.push(new userViewModel(data[i].Id, data[i].Name, data[i].StatusDescription, data[i].Email));
}
setupDatatable();
}
Suddenly it started working.
Hopefully, my stupidity and days worth of headaches will help someone else :)
I have this viewmodel, on my web I have a dropdown that updates the sortedallemployees option. It works fine except my table is empty initially. Once I sort the first time I get data. Seems like when the vm is created it doesn't wait for allemployees to be populated.
var vm = {
activate: activate,
allemployees: allemployees,
sortedallemployees:ko.computed( {
return allemployees.sort(function(f,s) {
var ID = SelectedOptionID();
var name = options[ ID - 1].OptionText;
if (f[name] == s[name]) {
return f[name] > s[name] ? 1 : f[name] < s[name] ? -1 : 0;
}
return f[name] > s[name] ? 1 : -1;
});
}
Without the rest of your code, its difficult to tell exactly how this will behave. That being said, you are doing several very odd things that I would recommend you avoid.
First, defining all but the simplest viewmodels as object literals will cause you pain. Anything with a function or a computed will almost certainly behave oddly, or more likely not at all, when defined this way.
I would recommend using a constructor function for your viewmodels.
var Viewmodel = function(activate, allEmployees) {
var self = this;
self.activate = activate;
self.allEmployees = ko.observableArray(allEmployees);
self.sortedEmployees = ko.computed(function() {
return self.allEmployees().sort(function(f,s) {
//your sort function
});
});
};
var vm = new Viewmodel(activate, allemployees);
This method has several advantages. First, it is reusable. Second, you can reference its properties properly during construction, such as during the computed definition. It is necessary for a computed to reference at least one observable property during definition for it to be reactive.
Your next problem is that your computed definition is not a function, but an object. It isn't even a legal object, it has a return in it. This code shouldn't even compile. This is just wrong. The Knockout Documentation is clear on this point: computed's are defined with a function.
Your last problem is that your sort function is referencing things outside the viewmodel: SelectedOptionID(). This won't necessarily stop it from working, but its generally bad practice.
I keep track of deleted objects in an observableArray called 'Deletions'. I parse that array in the UI to create 'undo deletion' links, but I can't get this to work. The code is very straight-forward and looks like this:
this.removePage = function(page){
self.formBuilder.pages.destroy(page);
var newDeletion = new Deletion();
newDeletion.element(page);
self.deletions.push(newDeletion);
}
this.removeFormElement = function(element){
self.formElements.destroy(element);
var newDeletion = new Deletion();
newDeletion.element(element);
builder.deletions.push(newDeletion);
}
var Deletion = function(){
var self = this;
this.element = ko.observable();
};
Note that different types of elements can be added to the Deletions observableArray. The only thing I need to do in the 'unremove' function, is setting the 'destroy' flag to false. But, I can't get that to work:
this.unremovePage = function(deletion){
deletion.element()._destroy(false);
}
What's the correct way of doing this?
EDIT
I can't get this working for the nested FormElements. The structure is: my main ViewModel is called 'FormBuilder'. The FormBuilder has multiple Pages (those are ViewModels themselves) and each Page has multiple FormElements (see code snippet above).
I can 'undelete' those FormElements, but I have no clue how to force a refresh on them.
this.unremove = function(deletion){
//console.log(deletion.element);
deletion.element()._destroy = false;
self.deletions.remove(deletion);
self.formBuilder.pages.valueHasMutated(); // works
deletion.element().valueHasMutated(); // this doesn't work
self.formBuilder.pages.indexOf(deletion.element()).valueHasMutated(); // neither does this
self.deletions.valueHasMutated(); // works
};
FormBuilder is the main ViewModel;
FormBuilder has an observableArray called Pages, each Page is a ViewModel;
Each Page has an observableArray called FormElements, each FormElement is a ViewModel;
FormBuilder has an observableArray called Deletions, each Deletion is a ViewModel and each Deletion contains an element, either a Page or a FormElement.
The problem:
I use the function 'unremove' to set the 'destroy' property of the element (either Page or FormElement) to false. As you can see, I then call 'valueHasUpdated' on pages. But how do I call that on the observableArray formElements as contained by an individual Page?
_destroy is not an observable. So, what you can do it set _destroy to false and then call valueHasMutated on the observableArray, so that any subscribers (the UI) knows that it may need to make updates.
So, you would want to deletion.element()._destroy = false; and then call self.deletions.valueHasMutated().