I'm stuck on one particular part of my project which consists of the components mentioned in the title.
I currently have a proof of concept that works the way I want it to work:
Sammy is integrated into the knockout viewmodels (as per the tutorial
on the knockout site)
the views are loaded on demand by a controller
(so I don't have to define every single view on the application page)
In my current situation I instance the viewmodels when the application starts (if I don't instance them, Sammy will not handle the routing). The problem is where the view is loaded and swapped by Sammy. I have to make a call to ko.applyBindings for KO to bind to the view. But its bad practice to repeatedly call applybingings.
My question, how do I bind to my views that are loaded on demand? I can't call ko.applybindings since that would create a memoryleak when the view is loaded more than once.
Here is an example VM with the offending ko.applyBindings:
function serviceInfoVm() {
var self = this;
self.ObjectKey = ko.observable();
self.Service = ko.observable();
self.LoadService = function () {
$.get('ServiceData/Detail', { serviceId: self.ObjectKey() }, function (data) {
self.Service(data);
});
};
$.sammy('#content', function () {
this.get('#/service/:id', function (context) {
var ctx = context;
self.ObjectKey(this.params['id']);
self.LoadService();
$.get('Content/ServiceInfo', function (view) {
ctx.app.swap(view);
ko.applyBindings(self);
});
});
}).run();
};
Anyone with some pointers and/or solutions to this problem?
You have the Sammy code in the viewmodel, which can work great if that viewmodel will be present and you want sub viewmodels and views to be loaded. So I assume that is what you are trying to do. Food for thought ... separate the sammy code into its own module (I call mine router in router.js) and let it manage the navigation separate from any viewmodel.
But back to your code ... you could set up your subviews and subviewmodels and use applybindings on them prior to the sammy.get being called. Basically, you are registering your routes in advance. Then the sammy.get just navigates to the new view, which is already data bound.
Not a solution but another approach:
Ended up abandoning the idea of loading the views dynamically.
Now my views are always present in the page and the visibility is triggered by this code:
var app = function () {
var self = this;
self.State = ko.observable('home');
self.Home = ko.observable(new homepageVm());
self.User = ko.observable(new userInfoVm());
$.sammy(function () {
this.get('#/', function (context) {
self.State('home');
});
this.get('#/info/:username', function (context) {
self.State('user');
self.User().UserName(context.params['username']);
self.User().LoadInfo();
});
}).run();
};
And the div visibility is triggered this way:
<div id="homeView" data-bind="with: Home, visible: State() === 'home'">
This way the ko.applyBindings only needs to be called once when the app starts.
The viewmodel above is bound to our shell page.
More on this here
Calling applyBindings on the specific element in the returned template is an option:
ko.applyBindings(viewModel, htmlNode)
Also see this question with regard to lazy loading templates: knockout.js - lazy loading of templates
And docs here for applyBindings: http://knockoutjs.com/documentation/observables.html
Related
I am trying to get a piece of code to work where I don't have total control over half of the code. In short, there is 1 main controller that has an object that is then obtained from a second controller. When main controller 1 updates the object, controller 2 never sees it. I think this is because the 2 controllers aren't watching the object/properties (copies?). If you notice, the Angular Binding to {{Title}}, this is where the issue is visible as the "Title" never gets updated in the second controller.
Here is some sample code that shows the problem. Currently, the code does a 3 seconds loop to get the object again, and reassign it to the second controller.
The code here I can't really touch. I have source, but it spaghetti and I am just generalizing what is here.
// html I can't really change, outside my world.
<div id="mainApp" ng-app="MainApp" ng-controller="mainController">
</div>
// code I can't "really" change, non-angular (can't use $http).
$ajax(get...)
.success(function (result) {
$('#element').html(result);
})
The code below is fairly separated and I can tinker with it. The HTML is returned from a service called by the $ajax call above.
// code I can change (the "result", or html returned from the service)
// containerController.js
var containerController = function ($scope, $timeout) {
$scope.models = {
item: null;
}
$scope.getItem = function() {
var mainAppScope = angular.element($('#mainApp')).scope();
$scope.models.item = mainAppScope.GetItem();
}
$scope.getItem();
// HACK WORK AROUND
// Get the item from the mainController.
var itemSync = setInterval(function () {
$scope.getItem();
$scope.$apply();
}, 3000);
}
The HTML returned from the service (it's really an ASP.NET MVC Partial View)
// HTML
<div id="container" ng-app="containerApp" ng-controller="containerController">
<!-- This will bind the first time, but won't syncronize when other controller updates -->
<!-- the controller is currently doing a loop to do so, not good. -->
<div>{{models.item.Title}}</div>
</div>
<script type="text/javascript" src="~/Scripts/controllers/containerController.js"></script>
<script type="text/javascript">
// This will inject the controller and new app into angular.
var container = document.getElementById('container');
var containerApp = angular.module('containerApp', []);
containerApp.controller('containerController', ['$scope', containerController]);
angular.bootstrap(angular.element(container), ['containerApp']);
</script>
You could change hacky part to use $interval which will manage to run digest cycle after each interval
var itemSync = $interval(function () {
$scope.getItem();
}, 3000);
To sync object from parent controller to child controller you could use object structure of model, that will is nothing but Javascript prototypal & will update data.
$scope.pageData = {}; //declare this in parent controller
$scope.pageData.title = 'Title 1' //use this where you want to change in child controller
I have two lists which are rendered by my directive. The requirement is that user can move an item from one list to another. I have a simplified implementation of this below:-
http://jsfiddle.net/yK7Lt/
The above shows a demo of how it should behave. Notice in this I manipulate the model and the DOM auto-syncs with it.
However, the problem is I am using jquery-ui-sortable plugin. So, the user can drag and drop the item from one list to another. Since jQuery is unaware of AngularJs so it modified the DOM. Now in my directive I have placed the code to sync the underlying model with the changed DOM.
The below jsfiddle code is a simplified version of my code.
http://jsfiddle.net/5Xuz2/1/
The relevant code snippet is:-
$('#btn').on('click', function () {
var li = $('#left li').first().detach();
$('#right').prepend(li);
console.log('moved top DOM to right list');
angular.element('#left').scope().$apply(function () {
// The moment this code runs, the DOM related to i is
// marked with $$NG_REMOVED, and is removed from page.
// Also somehow the DOM related to item D too is removed.
i = itemsl.shift(); // i is global variable.
});
angular.element('#right').scope().$apply(function () {
itemsr.unshift(i);
console.log('synced data with DOM');
});
});
The problem I am facing with my implementation is that the right list empties out as soon as I sync my left list model.
What is wrong with my implementation?
Is there a better approach?
the problem here is you are manipulating DOM with both Angular and jQuery... if you remove this piece of code
var li = $('#left li').first().detach();
$('#right').prepend(li);
it is working as expected
btw. I suggest trying angular-UI instead of jQueryUI
edit: OR you can try to refactor your code to something like this
var itemsl, itemsr, i, move;
function Model(name) {
this.name = name;
}
function Ctrl($scope) {
itemsl = $scope.itemsl = [new Model('A'), new Model('B'), new Model('C')];
itemsr = $scope.itemsr = [new Model('D')];
move = function() {
$scope.$apply(function() {
i = itemsl.slice(0,1);
itemsl.splice(0,1);
itemsr.unshift(i[0]);
i = null;
});
}
}
$(function () {
$('#btn').on('click', function () {
console.log('moved top DOM to right list');
move();
});
});
Couple of days ago I found this interesting post at http://www.smartjava.org/content/drag-and-drop-angularjs-using-jquery-ui and applied it into my website. However when I progressively using it there is a bug I identified, basically you can not move an item directly from one div to another's bottom, it has to go through the parts above and progress to the bottom. Anyone can suggest where does it goes wrong? The example is at http://www.smartjava.org/examples/dnd/double.html
Troubling me for days already.....
I did this a bit differently. Instead of attaching a jquery ui element inside the directive's controller, I instead did it inside the directive's link function. I came up with my solution, based on a blog post by Ben Farrell.
Note, that this is a Rails app, and I am using the acts_as_list gem to calculate positioning.
app.directive('sortable', function() {
return {
restrict: 'A',
link: function(scope, elt, attrs) {
// the card that will be moved
scope.movedCard = {};
return elt.sortable({
connectWith: ".deck",
revert: true,
items: '.card',
stop: function(evt, ui) {
return scope.$apply(function() {
// the deck the card is being moved to
// deck-id is an element attribute I defined
scope.movedCard.toDeck = parseInt(ui.item[0].parentElement.attributes['deck-id'].value);
// the id of the card being moved
// the card id is an attribute I definied
scope.movedCard.id = parseInt(ui.item[0].attributes['card-id'].value);
// edge case that handles a card being added to the end of the list
if (ui.item[0].nextElementSibling !== null) {
scope.movedCard.pos = parseInt(ui.item[0].nextElementSibling.attributes['card-pos'].value - 1);
} else {
// the card is being added to the very end of the list
scope.movedCard.pos = parseInt(ui.item[0].previousElementSibling.attributes['card-pos'].value + 1);
}
// broadcast to child scopes the movedCard event
return scope.$broadcast('movedCardEvent', scope.movedCard);
});
}
});
}
};
});
Important points
I utilize card attributes to store a card's id, deck, and position, in order to allow the jQuery sortable widget to grab onto.
After the stop event is called, I immediately execute a scope.$apply function to get back into, what Misko Hevery call,s the angular execution context.
I have a working example of this in action, up in a GitHub Repo of mine.
I have a page, which is used for building queries and running them against different entities (Kind of a query builder/generic search).
The results are displayed in JQGrid, so effectively the same grid will be used for rendering results from different entities.
This results grid has to support context menus, which will differ for each entity. So I need a way to change the context menu as per the entity. Each entity may have different number of menu items in context menu and each item may respond in a different manner (sometimes an alert, sometimes an action spawning in a different tab).
Rendering different menus (through li) is not an issue but attaching the methods to the li is proving to be a challenge. Any pointers will be highly appreciated.
I am using jquery.contextmenu-ui.js .
Following is from a sample that I picked from their (JQGrid) site
function initGrid() {
$("#EntityGrid").contextMenu('cMenu'
,{
bindings: { /* I would like to avoid this and pass all the actions to one method*/
'edit': function (t) {
editRow();
},
'add': function (t) {
addRow();
},
'del': function (t) {
delRow();
}
},
onContextMenu: function (event, menu) {
var rowId = $(event.target).parent("tr").attr("id")
var grid = $("#EntityGrid");
grid.setSelection(rowId);
return true;
}
}
);
}
Thanks,
Avinash
You can use onShowMenu callback of contextMenu instead of static binding using bindings. In the same way the menuId used as the first parameter of contextMenu could be the id of dynamically created div with empty <ul>. The onShowMenu has the form
onShowMenu: function (e, $menu) {
// here one can clear `<ul>` child of $menu
// and append it with "<li>" items
return $menu;
}
In the answer you will find an example of the code which build menu dynamically.
I am new to Backbone and started by working through the Todos example. After that I created a new version of the example, for Contacts rather than Todos, that uses a Ruby on Rails web app and and it's associated REST API rather than localstorage for persistence. After making the modifications I am able to successfully have the Backbone app update the Rails app but I cannot get the Backbone views to render the data that the Backbone app receives from the Rails REST API. I have stepped through the code in the debugger and can see that:
the events that call the functions to populate the views are being bound to the collection of models
when I fetch the model data the collection is getting updated with the data from the server
however, the reset event bound to the collection does not fire
Can anybody point me to what might be causing the reset event to not fire? My code is below:
Collection:
var ContactsList = Backbone.Collection.extend({
model: Contact,
url: 'http://localhost:3000/contacts.json',
});
var Contacts = new ContactsList;
AppView:
var AppView = Backbone.View.extend({
el: $("#contactapp"),
events: {
"keypress #new-contact": "createOnEnter"
},
initialize: function() {
this.input = this.$("#new-contact");
Contacts.bind('add', this.addOne, this);
Contacts.bind('reset', this.addAll, this);
Contacts.bind('all', this.render, this);
Contacts.fetch();
},
addOne: function(contact) {
var view = new ContactView({model: contact});
this.$("#contact-list").append(view.render().el);
},
addAll: function() {
Contacts.each(this.addOne);
},
createOnEnter: function(e) {
if (e.keyCode != 13) return;
if (!this.input.val()) return;
Contacts.create({first_name: this.input.val()});
this.input.val('');
},
});
var App = new AppView;
You're probably getting an empty jQuery selector returned by the el: $("#contactsapp") configuration in your view.
http://lostechies.com/derickbailey/2011/11/09/backbone-js-object-literals-views-events-jquery-and-el/
don't use jQuery selectors as object literal values. with Backbone's view, you can just provide the selector string:
el: "#contactsapp"
but this is a bad idea anyways. you should let the view render it's own el, and use other code to populate the "#contactsapp" element with the view:
$(function(){
view = new AppView();
view.render();
$("#contacts").html(view.el);
});
See the above link for more info.
After much debugging I found that the bound reset event was not being fired because I was using an old version of backbone.js. In the version I was using the refresh event was being fired not the reset event. Once I upgraded to the newer version of backbone.js the reset event fired