two-way binding on array does not work - ember.js

I have an array of strings that I render as a list of inputs.
Then there is a way to add a new item (new input) into this list.
However when change a text in the inputs and then add a new item, all the changes that were previously made to the old inputs are gone - and also the array of strings in my controller does not change.
Here is a code:
http://ember-twiddle.com/653be725890f234931dd
how can I preserve changes that are made to the "old inputs"?
I am quite suprised by this behaviour, since in angular this is a pretty common thing to do and it works out of the box... so I guess I must be doing something terribly wrong here :(

I tested a way that solved the problem, but maybe there's a better way, but it can create a function to perform the change in the value index and the report that there was a change in the controller, sorry the English Google translation.
Template:
{{#each valueList key="#index" as |value index|}}
<div>{{input value=value change=(action 'updateItem' index)}}</div>
{{/each}}
Controller:
actions : {
updateItem : function( index ) {
this.get( 'valueList' )[ index ] = event.target.value;
this.notifyPropertyChange( 'valueList' );
},
},

Related

Get the value of another select control in Ember

My situation is I have a table where the user can add suppliers, and edit any existing ones (so there are potentially multiple records). Three of the fields (Type, Name, Version) come from a lookup object returned by the API (which is one table in the backend database).
Before clicking 'edit'
In edit mode
The thing is, I need these select elements to be "chained" (or cascading), but since they're populated from the same object, it's more like the selection of "Type" will filter the options available for "Name" and likewise selecting Name will further restrict the options available for Version.
However, since this is just one record being edited, these selects are in an {{#each supplier in suppliers}} block to generate the rows, and show selects if that record's isEditing property is true, so the value or selection is the per-record value, e.g. supplier.type and not a single property on the whole controller.
I've tried to come up with multiple ways to do this, but so far haven't found a solution to cascading dropdowns with multiple records since that means the value of any one select is dependent on the record.
I think I could get the option filtering to work if I knew how to reference the value of say the Type dropdown from within the controller, but then again it's conceivable that two records could be in edit mode at once, so modifying any property on the controller to populate the selects would affect the others too, and that's not good. I just really wanted to figure this out so I didn't have to pop up a modal dialog to edit the record.
You should use components to handle each row seperately.
Let's say that you have something like this:
{{#each suppliers as |supplier|}}
// .. a lot of if's, selects and others
{{/each}}
If you find yourself using {{#each}} helper and your block passed to that helper is more than one line, than it's a good sign you probably need a component there.
If you create a component named, let's say, SupplierRow you could make it as follow:
module export Ember.Component({
editing: Ember.computed.alias('model.isEditing'),
types: Ember.computed('passedTypes', function() {
// .. return types array for that exact supplier
}),
names: Ember.computed('passedNames', 'model.type', function() {
// .. return names array for that exact supplier based on possibleNames and model.type
}),
versions: Ember.computed('passedVersions', 'model.type', 'model.name', function() {
// .. return versions array for that exact supplier based on possibleVersions and model.type and model.name
}),
actions: {
saveClicked() {
this.sendAction('save', this.get('model'));
}
}
});
The template would basically look similiary to what you have currently in your {{#each}} helper. It would be rendered something like this:
{{#each suppliers as |supplier|}}
{{supplier-row model=supplier possibleTypes=types possibleNames=names possibleVersions=versions save="save"}}
{{/each}}
Seems like you are using an old version of Ember wich allows context switching in {{#each}} helpers. Assuming that, you can set itemController for each iteration and handle selectable values for each row separately:
{{#each suppliers itemController="supplierController"}}
// this == supplierController, this.model == supplier
{{/each}}
So inside the supplierController you can calculate select content for each single supplier. You can also access main controller from item controller by this.parentController property.

Handlebars looping over an Ember component simple property

In an ember component how can I generate 6 things in the template, given a component property called num with value 6?
Do I have to create an array just for the purposes of this in the component? If so can someone advise the most reusable way to do this?
I think I would need to do this via a helper:
Ember.Handlebars.helper('highlight', function(value, options) {
var escaped = Handlebars.Utils.escapeExpression(value);
return new Ember.Handlebars.SafeString('<span class="highlight">' + escaped + '</span>');
});
So in my experience, this works well:
{{#each row in rows}}
//do something
{{/each}}
However, as you say, you'll probably need an array populated. I've used this with an array of Ember models to populate a table, but I'm sure you'll be able bend it to your purpose!

Binding to an array or object subscript

I'm wanting to do something that I suspect is quite easy but for some reason can't figure out how to get my head around it:
{{#each item in myArray}}
{{ui-input value=storeMe[#index]}}
{{/each}}
In the above case I'd be iterating through a numeric array of things and I want to store values of a UI component using the same numeric index.
Similarly it would be nice to be able to do the following:
{{#each item in myArrayOfObjects}}
{{ui-input value=storeMe[item.id]}}
{{/each}}
Where the storage device -- storeMe -- is a dictionary whose keys are determined by the id property of each item in array of objects.
In my particular use-case, I am asking the user to input a strategy for measuring body fat. I use a select box for that:
<div class="clearfix downer">
{{x-selectize
options=measurementStrategies
labelField="name"
placeholder="measurement strategy"
valueObject=measurementPoints
}}
</div>
By binding to the "valueObject" above I get back a simple array of measurement points which are relevant for the user chosen strategy (e.g., chest, thigh, lower back, etc.). I then iterate through these measurement points and want to have a value stored for each one:
<div class="downer">
{{#each point in measurementPoints}}
<div class="clearfix">
{{ui-number-input value=model.measurements[point]}}
</div>
{{/each}}
This doesn't work, of course, because apparently I can't bind to an offset property (aka, measurements[point]).
In many cases this type of problem doesn't matter because if I want to manipulate the structure I'm iterating over then the each loop provides the indirection. The problem comes when the storage property is hanging off of a different base than that which you are iterating over. So in my case, if I were actually manipulating point or a property hanging off of point this would be easy because point is an offset of measurementPoints but in my case I'm iterating measurementPoints and saving values to model.measurements.
That’s too much logic for Handlebars. You could create a computed property instead that pairs each item with each value in storeMe, so you can access them as pairs.
You could probably accomplish this with a custom helper, but it seems like a mess to me.
Here’s a rudimentary example, lacking detail on your problem domain:
storeMeItemsByIndex: function() {
var storeMe = this.get('storeMe');
return this.get('myArray').map(function(item, index) {
return storeMe[index];
});
}.property('myArray', 'storeMe'),
storeMeItemsById: function() {
var storeMe = this.get('storeMe');
return this.get('myArray').map(function(item) {
return storeMe[item.id];
});
}
Those would fit your example template code. However, you wouldn’t have access to both the value from storeMe and the item at the same time. If you want that, you could just construct object pairs:
storeMeItemsByIndex: function() {
var storeMe = this.get('storeMe');
return this.get('myArray').map(function(item, index) {
return {item: item, value: storeMe[index]};
});
}.property('myArray', 'storeMe'),
Or something like that.
With your more specific example, you could do something like this:
measurementPointsWithValues: function() {
var measurements = this.get('model.measurements');
return this.get('measurementPoints').map(function(point, index) {
return {point: point, value: measurements.itemAt(index)};
};
}
Then you’d use it in your template like this:
{{#each pointAndValue in measurementPointsWithValues}}
{{! some use of pointAndValue.point, probably}}
{{ui-number-input value=pointAndValue.value}}
{{/each}}
It’s clunky, but it works. I don’t know the details of your object model, but you may benefit from some intermediate objects.

setting #each property, proper use - splitting search results with EmberJs

Im trying to split search results into bulks of 5 so each bulk could be presented desperately.
my data:
Wacky.Video = Ember.Object.extend({
DATA: null
presenter : null
});
The presenter holds the page number on which the data should appear - e.g model[0-5] hold val. 1, model[6-10] hold val. 2 and so on...
my controller:
Wacky.SearchController = Ember.Controller.extend({
...
resPageNum: 1,
...
This property will de/increment whenever a page number will be changed (by button click).
my HTML code:
{{#each res in model}}
{{#if pagePresentor}}
<div class="results">
//DO STUFF WITH RES. OBJECT
</div>
{{/if}}
{{/each}}
finally "pagePresentor" is a property that needs to determine which bulk to present in the current iteration.
So far Iv got this:
pagePresentor: function(value){
return value.presenter == this.get('resPageNum');
}.property('resPageNum','#each.presentor')
But I guess Im using it wrong, because nothing is getting printed at all.
Could anyone please explain how to set this working or at least the base principal for what am I doing wrong here?
Thanks in advance.
pagePresentor: function(value){
return value.presenter == this.get('resPageNum');
}.property('resPageNum','#each.presentor')
Here #each.presentor is a mistake. You should specify observed property which is in the array in the following format: arrayName.#each.propertyName according to http://emberjs.com/guides/object-model/computed-properties-and-aggregate-data/. So, here the name of array which contains the objects with presentor property is missing.

Handling MouseUp on Slider in Ember.js

I've been working on incorporating a slider into a page in an app I'm working on. My first attempt was to use the following in my template:
<span class="value">{{scaledDistThreshold}}%</span>
{{input type="range" min="0" max="100" step="1" value=scaledDistThreshold}}
<button class="set" {{action "setDistThreshold"}}>Set</button>
This works; that is, I can use the slider and see the bound value change, and pressing the button will persist whatever value to the server.
What I'd like to do now is to remove the 'Set' button and persist on mouseup from the slider itself, and I'd prefer to do it with Ember as opposed to, say, a direct jQuery hook. So far, my searches for sliders using Ember has turned up basically nothing.
Any ideas how I might accomplish this? Thanks in advance!
FOLLOW-UP EDIT
Per Justin's answer, I was able to do the following:
App.InputRangeComponent = Em.TextField.extend({
type: 'range',
action: 'mouseUp',
mouseUp: function () {
var value = this.get('value');
this.sendAction('action', value);
}
});
I was then able to use this component in my markup as follows:
{{input-range min="0" max="100" step="1" value=scaledDistThreshold action="setDistThreshold"}}
The action I named gets called on mouse-up and passes along the current value of the slider, all as intended. Thanks again to Justin for the answer!
My first thought, and this isn't, strictly-speaking, an answer to your question, but another option might be just saving the bound value on change, and throttling it to only do so, say, once every quarter-second or so.
Now, as for answering your actual question:
(Please note: I'm still fairly new to Ember, so if what I say doesn't make sense, it's probably me, not you)
I didn't know that type="range" was an option for the {{input}} helper. Learn something new every day. :)
Take a look at the Ember docs on the {{input}} helper. When you use type="text", Ember is creating an instance of Ember.TextField. You have a couple of options:
You can reopen Ember.TextField and add a mouseup event to it.
You can create your own, new helper, that uses a new view (that you create) that extends Ember.TextField.
I'd recommend #2 for fairly obvious reasons (you probably don't want every textfield you make doing the same thing on mouseup).
SO, option 2.
I think this is how it would go down. If not, hopefully, it will at least point you in the right direction, or someone will correct me.
First, you'll want to create a new view that extends Ember.TextEdit. Let's call it "rangeSlider".
var rangeSlider = Ember.TextField.extend({
});
Here is where you should add your mouseup event:
App.RangeSlider = Ember.TextField.extend({
mouseUp: function(){
//Do stuff here
}
});
Now, if you don't want to use {{view "App.RangeSlider"}}, you can create a new helper:
Ember.Handlebars.helper('rangeSlider', App.RangeSlider);
Then you just have to use {{rangeSlider}} to insert that view.
I'm not 100% sure how adding attributes via your new helper works, though.
Looking at the ember.js code, the {{input}} helper had a bit more going for it at declaration:
Ember.Handlebars.registerHelper('input', function(options) {
Ember.assert('You can only pass attributes to the `input` helper, not arguments', arguments.length < 2);
var hash = options.hash,
types = options.hashTypes,
inputType = hash.type,
onEvent = hash.on;
delete hash.type;
delete hash.on;
if (inputType === 'checkbox') {
Ember.assert("{{input type='checkbox'}} does not support setting `value=someBooleanValue`; you must use `checked=someBooleanValue` instead.", options.hashTypes.value !== 'ID');
return Ember.Handlebars.helpers.view.call(this, Ember.Checkbox, options);
} else {
if (inputType) { hash.type = inputType; }
hash.onEvent = onEvent || 'enter';
return Ember.Handlebars.helpers.view.call(this, Ember.TextField, options);
}
});
Now that I think about it, I wonder if there's a way to just extend an existing helper... Not sure. I'm not seeing anything in the API.
Anyway, hopefully, this will point you in the right direction. There may be a totally easier way, but this was the first thing that popped into my head when I read your question. :)