I'm running into an issue with KnockoutJS where it appears the data-bindings aren't updating as expected.
In my view model, I have an array of objects with an Enabled property that I initialize to True, and have that property bound to enable on a checkbox with a click handler CheckItems:
self.Answers(data.filter(function (step) {
if (step.Type == 0) {
step.Enabled = true;
return true;
}
else {
return false;
}
}));
...
<ul data-bind="foreach: $root.Answers">
<input type="checkbox" data-bind="click: $root.CheckItems, ..., enable: Enabled>
</ul>
Everything work fine and dandy, until the CheckItems handler runs and I try to set the Enabled property to false:
self.CheckItems = function (step) {
...
self.Answers().forEach(function (option) {
<call my handler>.done(function (data) {
option.Enabled = data;
}
);
});
// EDIT: added valueHasMutated() call here
self.Answers.valueHasMutated();
return true;
}
When I hit return true, I can inspect the self.Answers() object and it shows that the answers that should be disabled have Enabled = false (correctly), but once we're the handler, none of the checkboxes are disabled, and clicking on any other checkbox present and going through the handler shows that the Enabled property seems to have been reset to True. I triple checked and made sure there's nothing else touching the Enabled property anywhere in the code between checkbox clicks.
The binding itself seems to be working, too, since when I switch the initial Enable set to false, all of the checkboxes are disabled.
I'm making changes to the self.Answers array various other places in the script as well, and those go through fine, when a change is made it goes into knockout-latest.debug.js and lands in the notifySubscribers function with the appropriate updates to make. Maybe there's something special about the click event for input fields in knockout?
Any ideas? Still fairly new to Knockout overall, but I thought this was the entire point, that I could update a property in the Answers() observable array and it would update in the corresponding UI.
Edit: based on comments I tried calling valueHasMutated() after the edits were made, but it still isn't updating properly.
Edit2: tried making a copy of the self.Answers array, doing the handler calls to update Enabled, then setting with self.Answers(newAnswers), and this had the same result.
I was advised that the issue was occurring because properties of the objects in CurrentAnswers() weren't observable as well. Since there weren't changes to the array itself, knockout wasn't registering that it had to rebind anything, which is why calling valueHasMutated() didn't change anything as the array looked exactly the same before and after CheckItems() from an array standpoint (also why updating with a copy of the array didn't work, same issue). When setting CurrentAnswers() to a blank array (self.CurrentAnswers([])), then setting it to the updated value, things changed properly since KO recognized the actual array changes.
The solution I ended up using was to utilize ko.mapping.fromJS() method to fill CurrentAnswers() with objects that had observable properties. After some syntax changes in the HTML and JS to properly reference the new observables, everything worked properly.
Related
I'm making a web app with Angular however I'm having trouble with the load button. So the user presses the load button, selects a save to load, and a new set of properties is loaded into forms that each constitute a step-item in a stepper. The forms array that loads all of the forms is 'reset' with this function:
private resetPropForms(): void {
this.propForms = [];
}
This is the function that receives as an argument a properties event and then sets the forms array:
onPropertiesEmitted(properties: Property[]): void {
this.resetPropForms();
this.resetDividedProperties();
this.resetUndividedProperties();
this.setDividedProperties( properties );
this.setForms();
this.setUndividedProperties( properties );
}
And this is what my template looks like:
<se-stepper
*ngIf="undividedProperties"
[linearMode]="false"
[(activeStepIndex)]="activeStepIndex"
>
<se-step-item
*ngFor="let form of propForms"
>
<app-properties-form
[propertiesForm]="form"
(change)="onPropertiesChanged($event)"
>
</app-properties-form>
</se-step-item>
</se-stepper>
Lastly, the view is updated but only after I go to the next step and come back. And if I save the properties then the correct value is sent to the backend even though the value displayed in the view is incorrect/not updated. Any ideas why this is happening. I tried using trackBy with a unique identifier for each form ( using a random number ) but that didn't work. I tried using ChangesRef and detectChanges() and that didn't work. When I leave out the *ngFor and just display the first form in the array it updates properly so that leads me to believe that this problem has something to do with the *ngFor.
edit:I'm pretty sure setTimeout() placed in onPropertiesEmitted() worked but I forgot where I put it to make it work and it seemed like a not-so-great solution to the problem
I Think the problem is that the array is the same and change detect won't detect changes, try this
private resetPropForms(): void {
//this.propForms = [];
//array.slice() returns new array
this.propForms = this.propForms.slice(0,0);
}
You could also reset the form before assigning new properties ( form.reset() )
On my SharePoint there is a website where a single list-item is loaded based on a user's selection with JavaScript and CSOM. This list item has a total of ~60 properties defined in it's list definition.
In HTML input fields the user can modify most of the properties after jQuery filled in the loaded properties to their corresponding input fields. When the "save" button is pressed, the properties are collected from the inputs via jQuery and put into a simple JS-object (itemProps):
var itemprops = {
'foo': $('#foo-input').val(),
'bar': $('#bar-input').val()
}
Then, the following function gets called:
function updateListItem(itemProps, onItemAdded, onItemError) {
var list = web.get_lists().getByTitle('ListTitle');
var listItem = list.getItemById(id);
for (var propName in itemProps) {
if (itemProps.hasOwnProperty(propName)) {
listItem.set_item(propName, itemProps[propName]);
}
}
listItem.update();
context.executeQueryAsync(
function() {
onItemAdded(listItem);
},
onItemError
);
}
Debugging shows me, that the data in itemProps are valid. But sometimes (I can't reproduce that effect deterministically) some properties get lost and when I look at the list item in the list on the SharePoint some of the properties are empty, as if itemProps had null or "" associated to that property. When I first tried to debug this I simply created an item and saved it (correctly, with all properties) and then loaded and saved it again without modification but some properties got lost.
Other properties get updated correctly and sometimes this doesn't happen at all.
Is there any way to make sure this effect does't occur or at least to detect it and retry updating the data, before the user's inputs get lost?
I notice you're not invoking context.load(listItem) before calling context.executeQueryAsync() which might cause issues with the listItem object's values being stale or dehydrated.
The code in your question looks like it should still be setting the specified values correctly on the list item, although there may be some code later on (such as in the onItemAdded function) that's running into false assumptions or subtle data differences from the inadequately loaded list item.
I am going through the Mithril tutorial and am having trouble understanding m.withAttr. The guide has the following line in the view layer:
m("input[type=checkbox]", {onclick: m.withAttr("checked", task.done), checked: task.done()})
I have two questions.
1) I understand that the first half, onclick: m.withAttr("checked", task.done) means, essentially:
'set task.done, using m.prop, to the value of the "checked" attribute'. But what is the purpose of the second half, checked: task.done()? It seems like it is simply repeating the first half.
2) As I went through the tutorial, I wanted to add the functionality of persisting my Todos to a persistence layer. I created a save function, so that I could refactor the line that I referenced above into something like:
m("input[type=checkbox]", { onclick: todo.vm.markAsDone.bind(todo.vm, task)})
And in my view-model, I had the function:
vm.markAsDone = function(todo) {
m.withAttr("checked", todo.done), checked: todo.done();
todo.save();
};
But this did not work; I get an Uncaught SyntaxError: Unexpected token : error. I think the problem is that the event is not being properly bound to the markAsDone function, so it doesn't understand the "checked" attribute; but I'm not sure how to fix this problem (if that even is the problem).
Thanks for any help.
Question 1
The second parameter of the m() function defines attributes on the HTML element, in this case an <input type=checkbox> will be decorated. (The exception is the special config field)
checked determines if the input checkbox is checked, so it is required to display the state of the task.
onclick is the event handler that will modify the state.
So the attributes do different things, therefore both of them are needed.
Question 2
Since markAsDone is passed a todo model, you don't have to do any m.withAttr call there. Simply modify the model, and let Mithril redraw the view. The redraw happens automatically if you call markAsDone through an event like onclick.
If you want more information about the redraw procedure, I summarized it in a previous SO question.
Edit: markAsDone will probably look like this:
vm.markAsDone = function(todo) {
todo.done(true);
todo.save();
};
It was always my understanding that .observes('someProperty') and .property('someProperty') worked exactly the same, except that the former is used for triggering function calls and the latter is used to keep object properties up to date.
But now I'm having a problem. My controller code looks like this:
_logChange: function(){
console.log('model array observer fired');
}.observes('model.#each'),
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.#each')
The observer and computed property both watch model.#each but for some reason, the observer fires on every model change and the property only updates TWICE before mysteriously going dead. statsData is calculated once on initial page load, and once on the first route transition, then after that, none of the transitions (with the changes in the underlying model they make) affect it.
What's going on here? Shouldn't they respond to change in the same way?
Note that I am using the statsData property in my template.
observers fire immediately, computed's fire as part of the run loop and scheduled in a debounced fashion. Currently all you're watching is that you add or remove an item to the collection, not whether or not a property on one of the items in the collection has changed. If you want to watch a particular property, you need to specify it.
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.#each.cost')
if you just want to watch the collection changing you should just use []
statsData: function(){
console.log('statsData being updated');
...
return someArray;
}.property('model.[]')
Thanks to the lovely folks on Ember IRC, I was able to figure it out. The problem was that I was passing statsData to a component, like this: {{common-statistics values=statsData}} and in the component, I had this function:
_validateValues: function(){
var values = this.get('values');
if(!values || !Ember.isArray(values) || values.length === 0)
{
this.set('values',[]);
}
}.on('willInsertElement')
which is, as you can see, setting values if it's not what the component is expecting. Unfortunately, this was affecting statsData on the controller as well, thanks to this JavaScript language feature. By setting statsData in the component, I was breaking the computed property on the controller.
So it was never a problem with Ember at all. I just failed to realize that object properties on Ember objects behave the same way they do on "regular JavaScript objects."
I have a complex form in ExtJS 4, where various parts of the form are dynamically enabled or disabled based on what the user selects from some of the form fields. Whenever I disable a form field, I also clear any value it currently has in it.
I have a model class representing the form. To load the form, I use form.loadRecord(model). To update my model when the user "submits" the form, I use model.set(form.getValues()).
The problem is that ext's getValues() implementation skips form fields that are disabled. This causes problems in my case, because some of form fields that have changed values are disabled (ie. form fields whose values I cleared when I disabled them). As a result, these fields are not updated (cleared) in the model when I call model.set(...).
What would be the best way to work around this problem? I've considered the following ideas, but none seems very good. If you have a better one, I'd like to hear it.
Clear the model (set all fields to undefined) before calling model.setValues(). Unfortunately, there is no model.clear() method, so this gets ugly quickly - I have to get all fields and iterate over them, clearing each one individually.
Clear model fields also when I disable and clear the form fields. This seems to violate separation of concerns and also means the model gets changed, even when the user chooses to cancel and not submit the form.
Override ext's implementation of form.getValues() to not skip disabled fields. This is even more ugly because the actual code that needs to be changed is in the Ext.form.field.Field class, not Ext.form.Basic.
Disabled fields are commonly (not only extjs) always excluded from post data. Instead set fields readonly. The mean difference between readonly and disabled fields is just that.
This is the solution that you exposed in the thrid point:
The only way you have to change this behaviour is override this method.
Ext.override('Ext.form.field.Field', {
getSubmitData: function() {
var me = this,
data = null;
if (!me.isFileUpload()) {
data = {};
data[me.getName()] = '' + me.getValue();
}
return data;
}
});
About your first point, isnĀ“t .reject(false) useful?
The latest option could be override the getSubmitData for every single field in your form as follow:
{
xtype: 'textfield',
getSubmitData: this.getSubmitDataMyOwnVersion
}
I realize this is an old post but I have run into this same issue. IMHO this is a rather serious issue because it can cause data problems without you knowing about it. In my case I also set several check boxes to false when disabled but because of the way this works they were being left as true behind the scenes.
As a work around I now loop through all the fields in the form and manually update the record for each one. It's more work but I don't have to override any classes and the loop is generic enough that it will continue to work if/when the form definition is changed.
var fields = this.getForm().getForm().getFields();
var record = this.getForm().getRecord();
for (var i = 0; i < fields.length; i++) {
var name = fields.items[i].name;
var value = fields.items[i].value;
record.set(name, value);
}
Note that Mark Wagoner's answer breaks any advanced components / features of other components since it takes the value directly rather then getting the getSubmitValue(). I had to slightly modify Iontivero's answer as that was not the class I found Extjs calling at least in 4.2.0.
Ext.define('YourAppName.override.Field', {
override: 'Ext.form.field.Base',
getSubmitData: function() {
var me = this,
data = null,
val;
if (me.submitValue && !me.isFileUpload()) {
val = me.getSubmitValue();
if (val !== null) {
data = {};
data[me.getName()] = val;
}
}
return data;
}
});
then in Ext.application:
requires: ['YourAppName.override.Field'],
I haven't encountered that problem so far, but I update my model using the update() method rather than the setValue(). Maybe it handles disabled fields differently? Or maybe I'm headed down a path to need this answer as well since we're just starting major testing? -- This is the basic usage of the Form.update method though, assuming form is an Ext.form.Panel and this.record is a model instance:
//Save record
form.updateRecord(this.record);
this.record.save();
this.record.commit();
If that doesn't work for you, I would suggest writing a similarly named method and extending the form panel to include it that gets the array of values then goes through each one and updates it only if it's not null.