knockout.js - help dealing with UI state changes when polling for updates - forms

I'm having a problem losing UI state changes after my observables change and was hoping for some suggestions.
First off, I'm polling my server for updates. Those messages are in my view model and the <ul> renders perfectly:
When my user clicks the "reply" or "assign to" buttons, I'm displaying a little form to perform those actions:
My problem at this point was that when my next polling call returned, the list re-binds and I lose the state of where the form should be open at. I went through adding view model properties for "currentQuestionID" so I could use a visible: binding and redisplay the form after binding.
Once that was complete, the form displays properly on the "current item" after rebinding but the form values are lost. That is to say, it rebinds, rebuilds the form elements, shows them, but any user input disappears (which of course makes sense since the HTML was just regenerated).
I attempted to follow the same pattern (using a value: binding to set the value and an event: {change: responseChanged} binding to update an observable with the values). The HTML fragment looks like this:
<form action="#" class="tb-reply-form" data-bind="visible: $root.showMenu($data, 'reply')">
<textarea id="tb-response" data-bind="value: $root.currentResponse, event: {keyup: $root.responseChanged}"></textarea>
<input type="button" id="tb-submitResponse" data-bind="click: $root.submitResponse, clickBubble: false" value="Send" />
</form>
<form action="#" class="tb-assign-form" data-bind="visible: $root.showMenu($data, 'assign')">
<select id="tb-assign" class="tb-assign" data-bind="value: $root.currentAssignee, options: $root.mediators, optionsText: 'full_name', optionsValue: 'access_token', optionsCaption: 'Select one...', event: {change: $root.assigneeChanged}">
</select>
<input type="button" id="tb-submitAssignment" data-bind="click: $root.submitAssignment, clickBubble: false" value="Assign"/>
</form>
Now, I end up with what seems like an infinite loop where setting the value causes change to happen, which in turn causes value... etc.
I thought "screw it" just move it out of the foreach... By moving the form outside of each <li> in the foreach: binding and doing a little DOM manipulation to move the form into the "current item", I figured I wouldn't lose user inputs.
replyForm.appendTo(theContainer).show();
It works up until the first poll return & rebind. Since the HTML is regenerated for the <ul>, the DOM no longer has my form and my attempt to grab it and do the .appendTo(container) does nothing. I suppose here, I might be able to copy the element into the active item instead of moving it?
So, this all seems like I'm missing something basic because someone has to have put a form into a foreach loop in knockout!
Does anybody have a strategy for maintaining form state inside a bound item in knockout?
Or, possibly, is there a way to make knockout NOT bind anything that's already bound and only generate "new" elements.
Finally, should I just scrap knockout for this and manually generate for "new items" myself when each polling call returns.
Just one last bit of info; if I set my polling interval to something like 30 seconds, all the bits "work" in that it submits, saves, rebinds, etc. I just need the form and it's contents to live through the rebinding.
Thanks a ton for any help!

Well, I figured it out on my own. And it's embarrassing.
Here is a partial bit of my VM code:
function TalkbackViewModel( id ) {
var self = this;
talkback.state.currentTalkbackId = "";
talkback.state.currentAction = "";
talkback.state.currentResponse = "";
talkback.state.currentAssignee = "";
self.talkbackQueue = ko.observableArray([]);
self.completeQueue = ko.observableArray([]);
self.mediators = ko.observableArray([]);
self.currentTalkbackId = ko.observable(talkback.state.currentTalkbackId);
self.currentAction = ko.observable(talkback.state.currentAction);
self.currentResponse = ko.observable(talkback.state.currentResponse);
self.currentAssignee = ko.observable(talkback.state.currentAssignee);
self.showActionForm = function(data, action) {
return ko.computed(function() {
var sameAction = (self.currentAction() == action);
var sameItem = (self.currentTalkbackId() == data.talkback_id());
return (sameAction && sameItem);
}, this);
};
self.replyToggle = function(model, event) {
// we're switching from one item to another. clear input values.
if (self.currentTalkbackId() != model.talkback_id() || self.currentAction() != "reply") {
self.currentResponse("");
self.currentAssignee("");
self.currentTalkbackId(model.talkback_id());
}
My first mistake was trying to treat the textarea & dropdown the same. I noticed the dropdown was saving value & reloading but stupidly tried to keep the code the same as the textarea and caused my own issue.
So...
First off, I went back to the using the $root view model properties for currentAssignee and currentResponse to store the values off and rebind using value: bindings on those controls.
Next, I needed to remove the event handlers:
event: { change: xxxChanged }
because they don't make sense (two way binding!!!!). The drop down value changes and updates automatically by using the value: binding.
The textarea ONLY updated on blur, causing me to think I needed onkeyup,onkeydown, etc. I got rid of those handlers because they were 1) wrong, 2) screwing up the value: binding creating an infinite loop.
I only needed this on the textarea to get up-to-date value updates to my viewmodel property:
valueUpdate: 'input'
At this point everything saves off & rebinds and I didn't lose my values but my caret position was incorrect in the textarea. I added a little code to handle that:
var item = element.find(".tb-assign");
var oldValue = item.val();
item.val('');
item.focus().val(oldValue);
Some browsers behave OK if you just do item.focus().val(item.val()); but i needed to actually cause the value to "change" in my case to get the caret at the end so I saved the value, cleared it, then restored it. I did this in the event handler for when the event data is returned to the browser:
$(window).on("talkback.retrieved", function(event, talkback_queue, complete_queue) {
var open_mappings = ko.mapping.fromJS(talkback_queue);
self.talkbackQueue(open_mappings);
if (talkback_queue) self.queueLength(talkback_queue.length);
var completed_mappings = ko.mapping.fromJS(complete_queue);
self.completeQueue(completed_mappings);
if (self.currentTalkbackId()) {
var element = $("li[talkbackId='" + self.currentTalkbackId() + "']");
if (talkback.state.currentAction == "assign") {
var item = element.find(".tb-assign");
var oldValue = item.val();
item.val('');
item.focus().val(oldValue);
} else {
var item = element.find(".tb-response");
var oldValue = item.val();
item.val('');
item.focus().val(oldValue);
}
}
}
);
So, my final issue is that if I used my observables in my method "clearing" the values when a new "current item" is selected (replyToggle & assignToggle), they don't seem to work.
self.currentResponse("");
self.currentAssignee("");
I cannot get the values to clear. I had to do some hack-fu and added the line below that to just work around it for now:
$(".tb-assign").val("");

Related

How to get contents on BindPopup

I am trying to get marker's .bindPopup content on click event so I can save it to localStorage. But it is not working properly for each marker.
L.marker([76.920614, -60.117188])
.addTo(map)
.bindPopup('<div><span class="claimed">DATA 1</span></div>')
.on('click', groupClick);
L.marker([77.841848, -31.289063])
.addTo(map)
.bindPopup('<div><span class="claimed">DATA 2/span></div>')
.on('click', groupClick)
function groupClick(event) {
var a = document.querySelector('.claimed').innerHTML;
console.log(a);
}
it would work on first click but on the second click on different marker, it will take the data from the first marker that i clicked instead of the second marker. In this case i have to click somewhere else on the map or click the popup close button first before i can click on the next marker to properly get the data. is there any fix on this?
PROBLEM:
you are selecting in your function only the first appearance of the class (not the clicked ones child) at
var a = document.querySelector('.claimed').innerHTML;
SOLUTION 1 (NOT RECOMMENDED):
You should use the this keyword, and the getPopup() and getContent() methods instead, your function should look something like:
function groupClick(event) {
var a = this.getPopup().getContent();
console.log(a);
}
This way you'll get the escaped html, so a much better and proper way is...
SOLUTION 2 (RECOMMENDED): if you store the necessary data in the markers options (instead of storing and getting html from popup), like this:
L.marker([40, 12], {data: 1, datastring: 'first'})
.addTo(map)
.bindPopup('ONE')
.on('click', groupClick);
Then you can access this options in your function this way:
function groupClick(event) {
var a = this.options.data + ' ' + this.options.datastring;
alert(a);
}
A working fiddle again.
EDIT: I have found a workaround for the desired logic. But still, you need to link the marker and its popup content, because:
Popup content is not a node in the DOM, so you cannot access it before it is opened with a user click.
So in my solution i store a simple integer in the marker options (divId: int), which is the unique id of the marker. In the popup content, the radio inputs have the same id concatenated with the desired string (<input type="radio" id="Item-10-0" name="Item-10" value="0" checked="">).
L.marker([40, 32], {
divId: 10
})
.addTo(map)
.bindPopup('<div id="2div" class="popup-todo"><input type="radio" id="Item-10-0" name="Item-10" value="0" checked=""><label for="Item-10-0">Claimed</label><input type="radio" id="Item-10-1" name="Item-10" value="1"><label for="Item-10-1">Unclaimed</label></div>')
.on('click', groupClick);
If the user clicks, the new node is already accessible, so you can select it and use its id and value.
function groupClick(event) {
var a = document.querySelector('input[name=Item-'+this.options.divId+']');
document.querySelector('#demo').innerHTML = a.id + ' ' + a.value;
}
The fiddle.

Polymer 2 - Perform action every time element is shown via iron-pages/iron-selector

I'm attempting to create a logout page that will work even after that element has been attached once to the DOM. This occurs when you get a login, then logout, then login again, and attempt to log back out.
For instance, the shell has
<iron-selector selected="[[page]]" attr-for-selected="name">
<a name="logout" href="[[rootPath]]logout">
<paper-icon-button icon="my-icons:sign-out" title="Logout" hidden$="[[!loggedIn]]"></paper-icon-button>
</a>
<a name="login" href="[[rootPath]]login">
<paper-icon-button icon="my-icons:sign-in" title="Login" hidden$="[[loggedIn]]"></paper-icon-button>
</a>
</iron-selector>
<<SNIP>>
<iron-pages selected="[[page]]" attr-for-selected="name" fallback-selection="view404" role="main">
<my-search name="search"></my-search>
<my-login name="login"></my-login>
<my-logout name="logout"></my-logout>
<my-view404 name="view404"></my-view404>
</iron-pages>
I also have an observer for page changes in the shell:
static get observers() {
return [
'_routePageChanged(routeData.page)',
];
}
_routePageChanged(page) {
this.loggedIn = MyApp._computeLogin();
if (this.loggedIn) {
this.page = page || 'search';
} else {
window.history.pushState({}, 'Login', '/login');
window.dispatchEvent(new CustomEvent('location-changed'));
sessionStorage.clear();
this.page = 'login';
}
}
This works well as when I click on the icon to logout, it attaches the my-logout element just fine and performs what in ready() or connectedCallback() just fine.
my-logout has
ready() {
super.ready();
this._performLogout();
}
The issue comes when, without refreshing the browser and causing a DOM refresh, you log back in and attempt to log out a second time. Since the DOM never cleared, my-logout is still attached, so neither ready() nor connectedCallback() fire.
I've figured out a way of working around this, but it feels very kludgy. Basically, I can add an event listener to the element that will perform this._performLogout(); when the icon is selected:
ready() {
super.ready();
this._performLogout();
document.addEventListener('iron-select', (event) => {
if (event.detail.item === this) {
this._performLogout();
}
});
}
Like I said, it works, but I dislike having a global event listener, plus I have to call the logout function the first time the element attaches and I have to listen as the listener isn't active till after the first time the element is attached.
There does not appear to be a "one size fits all" solution to this. The central question is, "Do you want the parent to tell the child, or for the child to listen on the parent?". The "answer" I came up with in the question works if you want to listen to the parent, but because I don't like the idea of a global event listener, the below is how to use <iron-pages> to tell a child element that it has been selected for viewing.
We add the selected-attribute property to <iron-pages>:
<iron-pages selected="[[page]]" attr-for-selected="name" selected-attribute="selected" fallback-selection="view404" role="main">
<my-search name="search"></my-search>
<my-login name="login"></my-login>
<my-logout name="logout"></my-logout>
<my-view404 name="view404"></my-view404>
</iron-pages>
Yes, this looks a little confusing considering the attr-for-selected property. attr-for-selected says, "What attribute should I match on these child elements with the value of my selected property?" So when I click on
<iron-selector selected="[[page]]" attr-for-selected="name">
<a name="logout" href="[[rootPath]]logout"><paper-icon-button icon="my-icons:sign-out" title="Logout" hidden$="[[!loggedIn]]"></paper-icon-button></a>
</iron-selector>
it will set the <my-logout> internally as the selected element and display it. What selected-attribute="selected" does is to set an attribute on the child element. If you look in the browser JS console, you will see that the element now looks like
<my-login name="login"></my-logout>
<my-logout name="login" class="iron-selected" selected></my-logout>
We can define an observer in that in the <my-logout> element that checks for changes
static get properties() {
return {
// Other properties
selected: {
type: Boolean,
value: false,
observer: '_selectedChanged',
},
};
}
_selectedChanged(selected) {
if (selected) {
this._performLogout();
}
}
The if statement is so that we only fire the logic when we are displayed, not when we leave. One advantage of this is that we don't care if the element has already been attached to the DOM or not. When <iron-selector>/<iron-pages> selects the <my-logout> the first time, the attribute is set, the element attaches, the observer fires, the observer sees that selected is now true (as opposed to the defined false) and runs the logic.

Capturing changes to model using angular-google-places-autocomplete

I'm using angular-google-places-autocomplete (https://github.com/kuhnza/angular-google-places-autocomplete) with Ionic but having problems capturing the selected option when using this directive.
I have the directive set up like this:
<!-- template -->
<input type="text" placeholder="Place search" g-places-autocomplete ng-model="locationSearchResult"/>
<h5>Result</h5>
<pre ng-bind="locationSearchResult | json"></pre>
My controller code is set up to watch for changes to the locationSearchResult model, and if it does change to save the new location to local storage:
// Controller
$scope.locationSearchResult = {};
$scope.$watch('locationSearchResult', function(newVal, oldVal) {
if (angular.equals(newVal, oldVal)) { return; }
$scope.$storage.loc = newVal;
$state.go('new-page');
});
When using the autocomplete it seems to work as expected - I get a list of predictions, and selecting a prediction from the list of predictions updates the text input with the name of the selected place, and the JSON data for the selected place displays under the result heading. But, the change doesn't seem to be picked up by the $scope.$watch in the controller.
As a result, I can't seem to be able to capture the search result data and do anything with it - like add it to the user session.
Maybe I'm just going about it the wrong way (though I used the same approach with ngAutocomplete and it worked ok).
Use the event that gets emitted in your controller.
$scope.$on('g-places-autocomplete:select', function (event, param) {
console.log(event);
console.log(param);
});

Knockout.js text binding - multiple spaces collapsed

It seems that when using Knockout's text binding, multiple spaces become collapsed into one. For example:
<textarea data-bind="value: Notes"></textarea>
<p data-bind="text: Notes"></p>
function VM() {
this.Notes = ko.observable();
}
var vm = new VM();
ko.applyBindings(vm);
Here is a fiddle to demonstrate this: http://jsfiddle.net/9rtL5/
I am finding that in jsfiddle, the spaces are compacted in Firefox, Chrome and IE9. Strangely though within my app IE9 does not compact them, but the others do.
My understanding is that Knockout uses an HTML text node to render the value. I found this related question on preserving spaces when creating a text node:
http://www.webdeveloper.com/forum/showthread.php?193107-RESOLVED-Insert-amp-nbsp-when-using-createTextNode%28%29
Should Knockout handle transforming spaces appropriately? I don't really want to use a custom binding handler for this.
I actually came across this in the context of the display text within a select, and only discovered that it also relates to a simple text binding while debugging that issue. I presume the select issue is the same.
What you are observing is normal behavior. When rendered in certain elements, the whitespace is trimmed. Knockout shouldn't be doing any automatic replacements, if I wanted to send a string to a server with leading/trailing spaces using knockout, it better make it there with those spaces.
You should create a binding handler to replace the spaces with no-breaking spaces so it can be rendered that way.
ko.bindingHandlers.spacedtext = {
replaceSpace: function (str) {
return new Array(str.length + 1).join('\xA0');
},
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var spacedValue = ko.computed(function () {
var value = ko.utils.unwrapObservable(valueAccessor()),
text = value && value.toString();
return text && text.replace(/^\s+|\s+$/gm, ko.bindingHandlers.spacedtext.replaceSpace);
});
ko.applyBindingsToNode(element, { text: spacedValue });
}
};
Demo

jquery / ajax form not passing button data

I thought the HTML spec stated that buttons click in a form pass their value, and button "not clicked" did not get passed. Like check boxes... I always check for the button value and sometimes I'll do different processing depending on which button was used to submit..
I have started using AJAX (specifically jquery) to submit my form data - but the button data is NEVER passed - is there something I'm missing? is there soemthing I can do to pass that data?
simple code might look like this
<form id="frmPost" method="post" action="page.php" class="bbForm" >
<input type="text" name="heading" id="heading" />
<input type="submit" name="btnA" value="Process It!" />
<input type="submit" name="btnB" value="Re-rout it somewhere Else!" />
</form>
<script>
$( function() { //once the doc has loaded
//handle the forms
$( '.bbForm' ).live( 'submit', function() { // catch the form's submit event
$.ajax({ // create an AJAX call...
data: $( this ).serialize(), // get the form data
type: $( this ).attr( 'method' ), // GET or POST
url: $( this ).attr( 'action' ), // the file to call
success: function( response ) { // on success..
$('#ui-tabs-1').html( response );
}
});
return false; // cancel original event to prevent form submitting
});
});
</script>
On the processing page - ONLY the "heading" field appears, neither the btnA or btnB regardless of whichever is clicked...
if it can't be 'fixed' can someone explain why the Ajax call doesn't follow "standard" form behavior?
thx
I found this to be an interesting issue so I figured I would do a bit of digging into the jquery source code and api documentation.
My findings:
Your issue has nothing to do with an ajax call and everything to do with the $.serialize() function. It simply is not coded to return <input type="submit"> or even <button type="submit"> I tried both. There is a regex expression that is run against the set of elements in the form to be serialized and it arbitrarily excludes the submit button unfortunately.
jQuery source code (I modified for debugging purposes but everything is still semantically intact):
serialize: function() {
var data = jQuery.param( this.serializeArray() );
return data;
},
serializeArray: function() {
var elementMap = this.map(function(){
return this.elements ? jQuery.makeArray( this.elements ) : this;
});
var filtered = elementMap.filter(function(){
var regexTest1= rselectTextarea.test( this.nodeName );
var regexTest2 = rinput.test( this.type ); //input submit will fail here thus never serialized as part of the form
var output = this.name && !this.disabled &&
( this.checked || regexTest2|| regexTest2);
return output;
});
var output = filtered.map(function( i, elem ){
var val = jQuery( this ).val();
return val == null ?
null :
jQuery.isArray( val ) ?
jQuery.map( val, function( val, i ){
return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
}) :
{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
}).get();
return output;
}
Now examining the jQuery documentation, you meet all the requirements for it to behave as expected (http://api.jquery.com/serialize/):
Note: Only "successful controls" are serialized to the string. No submit button value is serialized since the form was not submitted using a button. For a form element's value to be included in the serialized string, the element must have a name attribute. Values from checkboxes and radio buttons (inputs of type "radio" or "checkbox") are included only if they are checked. Data from file select elements is not serialized.
the "successful controls link branches out to the W3 spec and you definitely nailed the expected behavior on the spec.
Short lame answer: I think it is teh broken! Report for bug fix!!!
I've run into a rather unusual issue with this. I'm working on a project and have two separate php pages where one has html on the page separate from the php code and one is echoing html from inside php code. When I use the .serialize on the one that has the separate html code it works correctly. It sends my submit button value in its ajax call to another php page. But in the one with the html echoed from the php script I try to do the same thing and get completely different results. It will send all of the other info in the form but not the value of the submit button. All I need it to do is send whether or not I pushed "Delete" or "Update". I'm not asking for help (violating the rules of asking for help on another persons post) but I thought this info might be helpful in figuring out where the break down is occurring. I'll be looking for a solution and will post back here if I figure anything out.