Knockout bindings getting lost in Durandal app - durandal

I have a SPA in which I am using Durandal / KnockoutJS with knockout.validation.js. Inside the {view}.js I have this set
ko.validation.configure ({
decorateElement: true,
registerExtenders: true,
messagesOnModified: true,
insertMessages: false,
messageTemplate: null,
parseInputAttributes: true,
});
and inside one of the views I have
<input class="input-member"
type="text"
data-bind="value: memberno, validationOptions: { errorElementClass: 'input-validation-error' }"/>
When the view is first activated the element correctly has the style input-validation-error applied.
On subsequent loading of the view the css is not applied as I require. I can see in firebug that the input field now has this class applied input-member validationElement
I don't know where validationElement came from - but something got messed up with the knockout bindings. I have tried moving the validation config into the shell.js but the result is the same (and not a good idea anyway).
Edit:
So far looks like errorElementClass: 'input-validation-error' is not being reapplied to the element after navigation. If the field is modified-focused-cleared , the validation fires normally. validationElement is the placeholder for the errorElementClass
Update
Found this bit of info at the github site and seems to be what im after
in {view}.js
function activate() {
vm.memberno.isModified(false);
return true;
}
The above code seems to work for input fields but not for select fields. Another idea I'm looking at is adding a binding handler like so
ko.bindingHandlers.validationElement = {
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var valueIsValid = valueAccessor().isValid();
if (!valueIsValid) {
$(element).addClass("input-validation-error");
} else {
$(element).removeClass("input-validation-error");
}
}
}
which works for all selects and inputs but is always on. If there's a way to deactivate this binding until the first form submit fires I think that will do it.

There needs to be a way to re-apply the binding or cause the binding to update. Try putting some code in the view model's activate function that forces validation.

Related

How to use store.filter / store.find with Ember-Data to implement infinite scrolling?

This was originally posted on discuss.emberjs.com. See:
http://discuss.emberjs.com/t/what-is-the-proper-use-of-store-filter-store-find-for-infinite-scrolling/3798/2
but that site seems to get worse and worse as far as quality of content these days so I'm hoping StackOverflow can rescue me.
Intent: Build a page in ember with ember-data implementing infinite scrolling.
Background Knowledge: Based on the emberjs.com api docs on ember-data, specifically the store.filter and store.find methods ( see: http://emberjs.com/api/data/classes/DS.Store.html#method_filter ) I should be able to set the model hook of a route to the promise of a store filter operation. The response of the promise should be a filtered record array which is a an array of items from the store filtered by a filter function which is suppose to be constantly updated whenever new items are pushed into the store. By combining this with the store.find method which will push items into the store, the filteredRecordArray should automatically update with the new items thus updating the model and resulting in new items showing on the page.
For instance, assume we have a Questions Route, Controller and a model of type Question.
App.QuestionsRoute = Ember.Route.extend({
model: function (urlParams) {
return this.get('store').filter('question', function (q) {
return true;
});
}
});
Then we have a controller with some method that will call store.find, this could be triggered by some event/action whether it be detecting scroll events or the user explicitly clicking to load more, regardless this method would be called to load more questions.
Example:
App.QuestionsController = Ember.ArrayController.extend({
...
loadMore: function (offset) {
return this.get('store').find('question', { skip: currentOffset});
}
...
});
And the template to render the items:
...
{{#each question in controller}}
{{question.title}}
{{/each}}
...
Notice, that with this method we do NOT have to add a function to the store.find promise which explicitly calls this.get('model').pushObjects(questions); In fact, trying to do that once you have already returned a filter record array to the model does not work. Either we manage the content of the model manually, or we let ember-data do the work and I would very much like to let Ember-data do the work.
This is is a very clean API; however, it does not seem to work they way I've written it. Based on the documentation I cannot see anything wrong.
Using the Ember-Inspector tool from chrome I can see that the new questions from the second find call are loaded into the store under the 'question' type but the page does not refresh until I change routes and come back. It seems like the is simply a problem with observers, which made me think that this would be a bug in Ember-Data, but I didn't want to jump to conclusions like that until I asked to see if I'm using Ember-Data as intended.
If someone doesn't know exactly what is wrong but knows how to use store.push/pushMany to recreate this scenario in a jsbin that would also help too. I'm just not familiar with how to use the lower level methods on the store.
Help is much appreciated.
I just made this pattern work for myself, but in the "traditional" way, i.e. without using store.filter().
I managed the "loadMore" part in the router itself :
actions: {
loadMore: function () {
var model = this.controller.get('model'), route = this;
if (!this.get('loading')) {
this.set('loading', true);
this.store.find('question', {offset: model.get('length')}).then(function (records) {
model.addObjects(records);
route.set('loading', false);
});
}
}
}
Since you already tried the traditional way (from what I see in your post on discuss), it seems that the key part is to use addObjects() instead of pushObjects() as you did.
For the records, here is the relevant part of my view to trigger the loadMore action:
didInsertElement: function() {
var controller = this.get('controller');
$(window).on('scroll', function() {
if ($(window).scrollTop() > $(document).height() - ($(window).height()*2)) {
controller.send('loadMore');
}
});
},
willDestroyElement: function() {
$(window).off('scroll');
}
I am now looking to move the loading property to the controller so that I get a nice loader for the user.

select2 scroll to error not working with jquery validation and asp.net mvc

Posting this here in case someone else has the same problem...
When using a select2 dropdown in a C# MVC4 site, the page is not scrolled to the correct position when validation fails. Validation as such works and error scrolling also works for other controls, just not select2's. The reason AFAICS is that select2 replaces the original select with it's own markup and then set the original select as display:none. jquery.validate then has no valid target to scroll to.
We are using twitter bootstrap for styling, but I don't think it has any impact on this problem.
The jquery.validate documentation (as well as many answers here on StackOverflow) suggests that you use $.validator.setDefaults to assign the invalidHandler, but I couldn't get this to work in asp.net (it does work for focusInvalid however), probably due to us using the MS unobtrusive library. Instead I used this code in my jquery ready handler:
$(function() {
$.validator.setDefaults({
focusInvalid: false
});
function scrollToError(error, validator) {
var elem = $(validator.errorList[0].element);
if (elem.length) {
if (elem.is(':visible'))
return elem.offset().top - 16;
elem = elem.prev($(".select2-container"));
if (elem.length) {
return elem.offset().top - 16;
}
}
return 0; // scroll to top if all else fails
}
$('form').bind('invalid-form.validate', function(error, validator) {
// fix scrolling and validation for select2
if (!validator.numberOfInvalids())
return;
$('html, body').animate({
scrollTop: scrollToError(error, validator)
}, 500);
});
...
I set focusInvalid to false to disable and avoid conflict with the standard scroll and focus behavior.
The bind() call is used instead of the invalidHandler option and is the same as used by the validate plugin.
scrollToError() selects the first invalid element and returns the position to scroll to, either a normal visible element or a previous item with the 'select2-container' class (i.e a select2 element) or top of page if all else fails.
Standard behavior (showing validation errors etc) still works as before.
Hope this helps someone and if you have a better solution I would be very interested in knowing about it.

Durandal: Multiple Routes, One ViewModel/View

I have 3 routes: items/one, items/two, and items/three and they're all pointing to 'items' vm/view.
in the items.js activate function, I'm checking the url, and based on that, I'm changing a filter:
function activate(r) {
switch (r.routeInfo.url) {
case 'items/one': vm.filterType(1); break;
case 'items/two': vm.filterType(2); break;
case 'items/three': vm.filterType(3); break;
}
return init(); //returns a promise
}
The items view has a menu with buttons for one, two, and three.
Each button is linked to an action like this:
function clickOne() {
router.navigateTo('#/items/one');
}
function clickTwo() {
router.navigateTo('#/items/two');
}
function clickThree() {
router.navigateTo('#/items/three');
}
this all works and I get the right filter on the view. However, I've noticed that if I'm on 'one', and then go to 'two', the ko-bound variables update in 'real-time', that is, as they're changing, and before the activate promise resolves, which causes the transition to happen twice (as the data is being grabbed, and after the activate function returns).
This only happens in this scenario, where the view and viewmodel are the same as the previous one. I'm aware that this is a special case, and the router is probably handling the loading of the new route with areSameItem = true. I could split the VMs/Views into three and try to inherit from a base model, but I was hoping for a simpler solution.
I was able to solve the issue by simply removing the ko bindings before navigation using ko.cleanNode() on the items containing div.
Assuming that in your parent view you've a reference to router.activeItem with a transition e.g.
<!--ko compose: {model: router.activeItem,
afterCompose: router.afterCompose,
transition: 'entrance'} -->
<!--/ko-->
then the entrance transition happens on every route you've setup to filter the current view.
But this transition should probably only happen on first time visit and from that point on only the view should be updated with the filtered data. One way to accomplish that would be to setup an observable filterType and use filterType.subscribe to call router.navigateTowith the skip parameter.
Something along the line:
var filterType = ko.observable();
filterType.subscribe(function (val) {
// Create an entry in the history but don't activate the new route to prevent transition
// router plugin expects this without leading '/' dash.
router.navigateTo(location.pathname.substring(1) + '#items/' + filterType(), 'skip');
activate();
});
Please note that the router plugin expects skipRouteUrl without leading / slash to compare the context.path. https://github.com/BlueSpire/Durandal/blob/master/App/durandal/plugins/router.js#L402
Your experience might be different.
Last in order to support deep linking in activate:
function activate(routerdata) {
// deep linking
if (routerdata && routerdata.filterType && (routerdata.filterType !== filterType() ) ) {
filterType(routerdata.filterType);
}
return promise;
};

How do I determine open/closed state of a dijit dropdownbutton?

I'm using a dijit DropDownButton with an application I'm developing. As you know, if you click on the button once, a menu appears. Click again and it disappears. I can't seem to find this in the API documentation but is there a property I can read to tell me whether or not my DropDownButton is currently open or closed?
I'm trying to use a dojo.connect listener on the DropDownButton's OnClick event in order to perform another task, but only if the DropDownButton is clicked "closed."
THANK YOU!
Steve
I had a similar problem. I couldn't find such a property either, so I ended up adding a custom property dropDownIsOpen and overriding openDropDown() and closeDropDown() to update its value, like this:
myButton.dropDownIsOpen = false;
myButton.openDropDown = function () {
this.dropDownIsOpen = true;
this.inherited("openDropDown", arguments);
};
myButton.closeDropDown = function () {
this.dropDownIsOpen = false;
this.inherited("closeDropDown", arguments);
};
You may track it through its CSS classes. When the DropDown is open, the underlying DOM node that gets the focus (property focusNode) receives an additional class, dijitHasDropDownOpen. So, for your situation:
// assuming "d" is a dijit.DropDownButton
dojo.connect(d, 'onClick', function() {
if (dojo.hasClass(d.focusNode, 'dijitHasDropDownOpen') === false) {
performAnotherTask(); // this fires only if the menu is closed.
}
});
This example is for dojo 1.6.2, since you didn't specify your version. It can, of course, be converted easily for other versions.

ExtJS4 trigger doLayout on store load

I have a grid with store: cdStore defined. The grid's records are edited using a form which is bound to the grid data. When updating a record, I would like for the refreshed records to show in the grid.
Currently I have
handler : function() {
areaForm.getForm().submit({
params: { action: "update" }
});
cdStore.loadPage(cdStore.currentPage);
areaGrid.doLayout();
}
It seems like this fails sometimes and older data remains displayed in the grid - perhaps doLayout() is called before the page is fully loaded.
Can I trigger a doLayout on loadPage somehow?
// ...
cdStore.load({
callback: function(){areaGrid.doLayout();},
page: cdStore.currentPage
});
Update
I would appreciate a line or two with an explanation if you would be so kind
You said that "doLayout() is called before the page is fully loaded" and you were right. So the doLayout must be called after the data is loaded. The one way to do that is to use load method. You can pass array of options into this method:
store.load({
page: 2,
limit: 50,
// and
callback: function(){ /*do something*/ }
});
The function you pass as callback is called exactly after the data is loaded. So doLayout() put into callback produces correct behaviour.