I have the following drop-downs:
{{view SettingsApp.Select2SelectView
id="country-id"
contentBinding=currentCountries
optionValuePath="content.code"
optionLabelPath="content.withFlag"
selectionBinding=selectedCountry
prompt="Select a country ..."}}
...
{{view SettingsApp.Select2SelectView
id="city-id"
contentBinding=currentCities
selectionBinding=selectedCity
prompt="Select a city ..."}}
The bound properties are defined in a controller:
SettingsApp.ServicesEditController = SettingsApp.BaseEditController.extend(SettingsApp.ServicesMixin, {
needs : ['servicesIndex'],
selectedCountry : null,
selectedCity : null,
currentCountries : null,
currentCities : null,
init : function () {
this._super();
},
setupController : function (entry) {
this._super(entry);
var locator = SettingsApp.locators.getLocator(this.get('content.properties.locator'));
var countryCode = locator.get('country'), city = locator.get('city');
this.set('currentCountries', SettingsApp.countries.getCountries());
this.set('currentCities', SettingsApp.locators.getCities(countryCode));
this.set('selectedCountry', SettingsApp.countries.getCountry(countryCode));
this.set('selectedCity', city);
// Add observers now, once everything is setup
this.addObserver('selectedCountry', this.selectedCountryChanged);
},
selectedCountryChanged: function () {
var countryCode = this.get('selectedCountry.code');
var currentCities = SettingsApp.locators.getCities(countryCode);
var selectedCity = currentCities[0];
this.set('currentCities', currentCities);
this.set('selectedCity', selectedCity);
},
...
});
Initial setup is working fine, but changing the country selection does not update the city selection in the drop-down, even though the observer (selectedCountryChanged) is called and the this.set('selectedCity', selectedCity); is working as expected (as seen in console logging). The currentCities are properly set after the observer runs, but the active (selected) value is not correct (remains unchanged).
Are there any known issues with the programmatic update of bound properties, in this case selectionBinding?
My Select2SelectView is:
SettingsApp.Select2SelectView = Ember.Select.extend({
prompt: 'Please select...',
classNames: ['input-xlarge'],
didInsertElement: function() {
Ember.run.scheduleOnce('afterRender', this, 'processChildElements');
},
processChildElements: function() {
this.$().select2({
// do here any configuration of the
// select2 component
escapeMarkup: function (m) { return m; } // we do not want to escape markup since we are displaying html in results
});
},
willDestroyElement: function () {
this.$().select2('destroy');
}
});
Check whether selected city is getting displayed by removing select2(replacing with normal select). If that's the case, selectionbinding has to be propagated to select2.
Related
I have a simple model backed controller with a simple validation on fullname. I added the validation to the models error computed property as shown below. It' works great except that this computed property is "fired" right away showing the "please enter a username" error right when the form is rendered.
Question is -how should I so this to get the nice computed property/2 way data bound property and error message but ... something that won't fire right away (instead waiting for the user to type something first).
var UserController = Ember.Controller.extend({
actions: {
submit: function() {
//verify model is legit ... transitionToRoute if so
}
}
});
var User = Ember.Object.extend({
enteredUsername: "",
username: function() {
var enteredUsername = this.get("enteredUsername");
return enteredUsername.trim();
}.property("enteredUsername"),
usernameError: function() {
var username = this.get("username");
if (!username) {
return "please enter a username";
}
}.property("username")
});
{{input type="text" value=model.enteredUsername}}
<span class="input-error">{{model.usernameError}}</span>
Like #Sisir said, you may need to have some sort of variable to check if the model value is dirty. Here is a way to get that implemented. Basically a model property will comprise of a value and isDirty property. So enteredUsername will be
enteredUsername: {
value: '',
isDirty: false
}
Your modified code will look like
var User = Ember.Object.extend({
enteredUsername: {
value: '',
isDirty: false
},
username: function() {
var value = this.get("enteredUsername.value").trim();
//Once dirty is set, then dont reset.
if(!this.get('enteredUsername.isDirty')) {
this.set('enteredUsername.isDirty', value.length > 0);
}
return value;
}.property('enteredUsername.value'),
usernameError: function() {
var username = this.get("username");
var isDirty = this.get('enteredUsername.isDirty');
if (!username && isDirty) {
return "please enter a username";
}
}.property('username', 'enteredUsername.isDirty')
});
Here is a working demo.
I am trying to create an interface for traversing tables in a relation database. Each select represents a column. If the column is a foreign key, a new select is added to the right. This keeps happening for every foreign key that the user accesses. The number of selects is dynamic.
I made a buggy implementation that has code that manually adds and removes select views. I think it probably can be replaced with better Ember code (some kind of array object maybe?), I'm just not sure how to best use the framework for this problem.
Here's my JSBin http://jsbin.com/olefUMAr/3/edit
HTML:
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Ember template" />
<meta charset=utf-8 />
<title>JS Bin</title>
<script src="http://code.jquery.com/jquery-1.9.0.js"></script>
<script src="http://builds.emberjs.com/handlebars-1.0.0.js"></script>
<script src="http://builds.emberjs.com/tags/v1.1.2/ember.js"></script>
</head>
<body>
<script type="text/x-handlebars" data-template-name="my_template">
{{view fieldSelects}}
</script>
<div id="main"></div>
</body>
</html>
JavaScript:
App = Ember.Application.create();
var TemplatedViewController = Ember.Object.extend({
templateFunction: null,
viewArgs: null,
viewBaseClass: Ember.View,
view: function () {
var controller = this;
var viewArgs = this.get('viewArgs') || {};
var args = {
template: controller.get('templateFunction'),
controller: controller
};
args = $.extend(viewArgs, args);
return this.get('viewBaseClass').extend(args);
}.property('templateFunction', 'viewArgs'),
appendView: function (selector) {
this.get('view').create().appendTo(selector);
},
appendViewToBody: function () {
this.get('view').create().append();
}
});
var DATA = {};
DATA.model_data = {
"Book": {
"fields": [
"id",
"title",
"publication_year",
"authors"
],
"meta": {
"id": {},
"title": {},
"publication_year": {},
"authors": {
"model": "Author"
}
}
},
"Author": {
"fields": [
"id",
"first_name",
"last_name",
"books"
],
"meta": {
"id": {},
"first_name": {},
"last_name": {},
"books": {
"model": "Book"
}
}
}
};
var Controller = TemplatedViewController.extend({
view: function () {
var controller = this;
return this.get('viewBaseClass').extend({
controller: controller,
templateName: 'my_template'
});
}.property(),
selectedFields: null,
fieldSelects: function () {
var filter = this;
return Ember.ContainerView.extend({
controller: this,
childViews: function () {
var that = this;
var selectedFields = filter.get('selectedFields');
var ret = [];
var model = 'Book';
selectedFields.forEach(function (item, index, enumerable) {
var selection = item;
if (model) {
var select = that.makeSelect(model, that.getPositionIndex(), selection, true).create();
ret.pushObject(select);
model = DATA.model_data[model].meta[selection].model;
}
});
return ret;
}.property(),
nextPositionIndex: 0,
incrementPositionIndex: function () {
this.set('nextPositionIndex', this.get('nextPositionIndex') + 1);
},
getPositionIndex: function () {
var index = this.get('nextPositionIndex');
this.incrementPositionIndex();
return index;
},
setNextPositionIndex: function (newValue) {
this.set('nextPositionIndex', newValue+1);
},
makeSelect: function (modelName, positionIndex, selection, isInitializing) {
var view = this;
return Ember.Select.extend({
positionIndex: positionIndex,
controller: filter,
content: DATA.model_data[modelName].fields,
prompt: '---------',
selection: selection || null,
selectionChanged: function () {
var field = this.get('selection');
// Remove child views after this one
var lastIndex = view.get('length') - 1;
if (lastIndex > this.get('positionIndex')) {
view.removeAt(this.get('positionIndex')+1, lastIndex-this.get('positionIndex'));
view.setNextPositionIndex(this.get('positionIndex'));
}
if (! isInitializing && DATA.model_data[modelName].meta[field].model) {
var relatedModel = DATA.model_data[modelName].meta[field].model;
view.pushObject(view.makeSelect(relatedModel, view.getPositionIndex()).create());
}
// Reset ``isInitializing`` after the first run
if (isInitializing) {
isInitializing = false;
}
var selectedFields = [];
view.get('childViews').forEach(function (item, index, enumerable) {
var childView = item;
var selection = childView.get('selection');
selectedFields.pushObject(selection);
});
filter.set('selectedFields', selectedFields);
}.observes('selection')
});
}
});
}.property()
});
var controller = Controller.create({
selectedFields: ['authors', 'first_name']
});
$(function () {
controller.appendView('#main');
});
Approach:
I would tackle this problem using an Ember Component.
I have used a component because it will be:
Easily reusable
The code is self contained, and has no external requirements on any of your other code.
We can use plain javascript to create the view. Plain javascript should make the code flow easier to understand (because you don't have to know what Ember is doing with extended objects behind the scenes), and it will have less overhead.
Demo:
I have created this JSBin here, of the code below.
Usage
Add to your handlebars template:
{{select-filter-box data=model selected=selected}}
Create a select-filter-box tag and then bind your model to the data attribute, and your selected value array to the selected attribute.
The application:
App = Ember.Application.create();
App.ApplicationController = Ember.ObjectController.extend({
model: DATA.model_data,
selected: ['Author','']
});
App.SelectFilterBoxComponent = Ember.Component.extend({
template: Ember.Handlebars.compile(''), // Blank template
data: null,
lastCount: 0,
selected: [],
selectedChanged: function(){
// Properties required to build view
var p = this.getProperties("elementId", "data", "lastCount", "selected");
// Used to gain context of controller in on selected changed event
var controller = this;
// Check there is at least one property. I.e. the base model.
var length = p.selected.length;
if(length > 1){
var currentModelName = p.selected[0];
var type = {};
// This function will return an existing select box or create new
var getOrCreate = function(idx){
// Determine the id of the select box
var id = p.elementId + "_" + idx;
// Try get the select box if it exists
var select = $("#" + id);
if(select.length === 0){
// Create select box
select = $("<select id='" + id +"'></select>");
// Action to take if select is changed. State is made available through evt.data
select.on("change", { controller: controller, index: idx }, function(evt){
// Restore the state
var controller = evt.data.controller;
var index = evt.data.index;
var selected = controller.get("selected");
// The selected field
var fieldName = $(this).val();
// Update the selected
selected = selected.slice(0, index);
selected.push(fieldName);
controller.set("selected", selected);
});
// Add it to the component container
$("#" + p.elementId).append(select);
}
return select;
};
// Add the options to the select box
var populate = function(select){
// Only populate the select box if it doesn't have the correct model
if(select.data("type")==currentModelName)
return;
// Clear any existing options
select.html("");
// Get the field from the model
var fields = p.data[currentModelName].fields;
// Add default empty option
select.append($("<option value=''>------</option>"));
// Add the fields to the select box
for(var f = 0; f < fields.length; f++)
select.append($("<option>" + fields[f] + "</option>"));
// Set the model type on the select
select.data("type", currentModelName);
};
var setModelNameFromFieldName = function(fieldName){
// Get the field type from current model meta
type = p.data[currentModelName].meta[fieldName];
// Set the current model
currentModelName = (type !== undefined && type.model !== undefined) ? type.model : null;
};
// Remove any unneeded select boxes. I.e. where the number of selects exceed the selected length
if(p.lastCount > length)
for(var i=length; i < p.lastCount; i++)
$("#" + p.elementId + "_" + i).remove();
this.set("lastCount", length);
// Loop through all of the selected, to build view
for(var s = 1; s < length; s++)
{
// Get or Create select box at index s
var select = getOrCreate(s);
// Populate the model fields to the selectbox, if required
populate(select);
// Current selected
var field = p.selected[s];
// Ensure correct value is selected
select.val(field);
// Set the model for next iteration
setModelNameFromFieldName(field);
if(s === length - 1 && type !== undefined && type.model !== undefined)
{
p.selected.push('');
this.notifyPropertyChange("selected");
}
}
}
}.observes("selected"),
didInsertElement: function(){
this.selectedChanged();
}
});
How it works
The component takes the two parameters model and selected then binds an observer onto the selected property. Any time the selection is changed either through user interaction with the select boxes, or by the property bound to selected the view will be redetermined.
The code uses the following approach:
Determine if the selection array (selected) is greater than 1. (Because the first value needs to be the base model).
Loop round all the selected fields i, starting at index 1.
Determine if select box i exists. If not create a select box.
Determine if select box i has the right model fields based on the current populated model. If yes, do nothing, if not populate the fields.
Set the current value of the select box.
If we are the last select box and the field selected links to a model, then push a blank value onto the selection, to trigger next drop down.
When a select box is created, an onchange handler is hooked up to update the selected value by slicing the selected array right of the current index and adding its own value. This will cause the view to change as required.
A property count keeps track of the previous selected's length, so if a change is made to a selection that decreases the current selected values length, then the unneeded select boxes can be removed.
The source code is commented, and I hope it is clear, if you have any questions of queries with how it works, feel free to ask, and I will try to explain it better.
Your Model:
Having looked at your model, have you considered simplifying it to below? I appreciate that you may not be able to, for other reasons beyond the scope of the question. Just a thought.
DATA.model_data = {
"Book": {
"id": {},
"title": {},
"publication_year": {},
"authors": { "model": "Author" }
},
"Author": {
"id": {},
"first_name": {},
"last_name": {},
"books": { "model": "Book" }
}
};
So field names would be read off the object keys, and the value would be the meta data.
I hope you find this useful. Let me know if you have any questions, or issues.
The Controller:
You can use any controller you want with this component. In my demo of the component I used Ember's built in ApplicationController for simplicity.
Explaination of notifyPropertyChange():
This is called because when we are inserting an new string into the selected array, using the push functionality of arrays.
I have used the push method because this is the most efficient way to add a new entry into an existing array.
While Ember does have a pushObject method that is supposed to take care of the notification as well, I couldn't get it to honour this. So this.notifyPropertyChange("selected"); tells Ember that we updated the array. However I'm hoping that's not a dealbreaker.
Alternative to Ember Component - Implemented as a View
If you don't wish to use it in Component format, you could implement it as a view. It ultimately achieves the same goal, but this may be a more familiar design pattern to you.
See this JSBin for implementation as a View. I won't include the full code here, because some of it is the same as above, you can see it in the JSBin
Usage:
Create an instance of App.SelectFilterBoxView, with a controller that has a data and selected property:
var myView = App.SelectFilterBoxView.create({
controller: Ember.Object.create({
data: DATA.model_data,
selected: ['Author','']
})
});
Then append the view as required, such as to #main.
myView.appendTo("#main");
Unfortunately your code doesn't run, even after adding Ember as a library in your JSFiddle, but ContainerView is probably what you're looking for: http://emberjs.com/api/classes/Ember.ContainerView.html as those views can be dynamically added/removed.
this.$().remove() or this.$().append() are probably what you're looking for:
Ember docs.
I've created a typeahead view and i'm trying to send an action to the current controller to set a property. Here is my typeahead view
App.Typeahead = Ember.TextField.extend({
dataset_name: undefined, //The string used to identify the dataset. Used by typeahead.js to cache intelligently.
dataset_limit: 5, //The max number of suggestions from the dataset to display for a given query. Defaults to 5.
dataset_template: undefined, //The template used to render suggestions. Can be a string or a precompiled template. If not provided, suggestions will render as their value contained in a <p> element (i.e. <p>value</p>).
dataset_engine: undefined, //The template engine used to compile/render template if it is a string. Any engine can use used as long as it adheres to the expected API. Required if template is a string.
dataset_local: undefined, //An array of datums.
dataset_prefetch: undefined, //Can be a URL to a JSON file containing an array of datums or, if more configurability is needed, a prefetch options object.
dataset_remote: undefined, //Can be a URL to fetch suggestions from when the data provided by local and prefetch is insufficient or, if more configurability is needed, a remote options object.
ctrl_action: undefined,
didInsertElement: function () {
this._super();
var self = this;
Ember.run.schedule('actions', this, function () {
self.$().typeahead({
name: self.get('dataset_name'),
limit: self.get('dataset_limit'),
template: self.get('dataset_template'),
engine: self.get('dataset_engine'),
local: self.get('dataset_local'),
prefetch: self.get('dataset_prefetch'),
remote: self.get('dataset_remote')
}).on('typeahead:selected', function (ev, datum) {
self.selected(datum);
});
});
},
willDestroyElement: function () {
this._super();
this.$().typeahead('destroy');
},
selected: function(datum) {
this.get('controller').send(this.get('ctrl_action'), datum);
}
});
Here's an implementation
App.CompanyTA = App.Typeahead.extend({
dataset_limit: 10,
dataset_engine: Hogan,
dataset_template: '<p><strong>{{value}}</strong> - {{year}}</p>',
dataset_prefetch: '../js/stubs/post_1960.json',
ctrl_action: 'setCompanyDatum',
selected: function (datum) {
this._super(datum);
this.set('value', datum.value);
}
});
and here's my controller
App.PeopleNewController = Ember.ObjectController.extend({
//content: Ember.Object.create(),
firstName: '',
lastName: '',
city: '',
state: '',
ta_datum: undefined,
actions: {
doneEditing: function () {
var firstName = this.get('firstName');
if (!firstName.trim()) { return; }
var lastName = this.get('lastName');
if (!lastName.trim()) { return; }
var city = this.get('city');
if (!city.trim()) { return; }
var state = this.get('state');
if (!state.trim()) { return; }
var test = this.get('ta_datum');
// Create the new person model
var person = this.store.createRecord('person', {
firstName: firstName,
lastName: lastName,
city: city,
state: state
});
// Clear the fields
this.set('firstName', '');
this.set('lastName', '');
this.set('city', '');
this.set('state', '');
// Save the new model
person.save();
},
setCompanyDatum: function(datum) {
this.set('ta_datum', datum);
}
}
});
I'm expecting the setCompanyDatum controller action to be called, but it's not. Everything else is working as expected. The App.Typeahead.selected method is being called with the right action name, but it doesn't actually call the action method.
the controller inside your App.Typeahead points to the instance of the App.Typeahead, not the controller from the route where you are creating the view.
You should just be using sendAction
http://emberjs.jsbin.com/EduDitE/2/edit
{{view App.Typeahead}}
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
},
actions:{
externalAction:function(item){
console.log('helllllo' + item);
}
}
});
App.Typeahead = Ember.TextField.extend({
internalAction: 'externalAction',
didInsertElement: function () {
this.sendAction('internalAction', " I'm a sent action");
this._super();
}
});
I have a view with disabled selector that should be set to the value from ember-data as soon as the model is loaded.
LocationSelectView: Ember.Select.extend({
prompt: "Choose location",
contentBinding: 'controller.locations',
optionValuePath: 'content.id',
optionLabelPath: 'content.title',
valueBinding: 'controller.content.location_id'
})
and the view is disabled in template
{{view view.LocationSelectView disabled="true"}}
everything is working as expected as long as the locations were already loaded to the store,
if they are not loaded, the content is binding as expected (I can verify that by enabling the selector), but the selected value stays at the "prompt".
I worked around this by pre-loading the locations data in the controller's init, but I really don't like this solution.
how can I fix this ? is it a bug ?
This issue is mentioned here: https://github.com/emberjs/ember.js/issues/1333
My solution was to add an observer to the view like this:
preselect: function () {
var item = this.get('items.firstObject');
this.set('currentItem', item);
}.observes('items.#each')
That presumes using selectionBinding (which I prefer) instead of valueBinding
I developed a workaround for this problem. Look at the following code:
App.Select = Ember.Select.extend({
placeholder: '',
allowClear: false,
contentChanged: function() {
if (this.get('value') === undefined && this.get('_iv') != undefined) {
var v = this.get('_iv');
var o = this.get('content').findProperty('id', v);
if (o) {
this.set('value', v);
}
}
}.observes('content.#each'),
init: function() {
this._super();
this.set('_iv', this.get('value'));
},
...
I hope it helps
How do i change an elements class on click via ember.js, AKA:
<div class="row" {{bindAttr class="isEnabled:enabled:disabled"}}>
View:
SearchDropdown.SearchResultV = Ember.View.extend(Ember.Metamorph, {
isEnabled: false,
click: function(){
window.alert(true);
this.isEnabled = true;
}
});
The click event works as window alert happens, I just cant get the binding to.
The class is bound correctly, but the isEnabled property should be modified only with a .set call such as this.set('isEnabled', true) and accessed only with this.get('isEnabled'). This is an Ember convention in support of first-class bindings and computed properties.
In your view you will bind to a className. I have the following view in my app:
EurekaJ.TabItemView = Ember.View.extend(Ember.TargetActionSupport, {
content: null,
tagName: 'li',
classNameBindings: "isSelected",
isSelected: function() {
return this.get('controller').get('selectedTab').get('tabId') == this.get('tab').get('tabId');
}.property('controller.selectedTab'),
click: function() {
this.get('controller').set('selectedTab', this.get('tab'));
if (this.get('tab').get('tabState')) {
EurekaJ.router.transitionTo(this.get('tab').get('tabState'));
}
},
template: Ember.Handlebars.compile('<div class="featureTabTop"></div>{{tab.tabName}}')
});
Here, you have bound your className to whatever the "isSelected" property returns. This is only true if the views' controller's selected tab ID is the same as this views' tab ID.
The code will append a CSS class name of "is-selected" when the view is selected.
If you want to see the code in context, the code is on GitHub: https://github.com/joachimhs/EurekaJ/blob/netty-ember/EurekaJ.View/src/main/webapp/js/app/views.js#L100
Good answers, however I went down a different route:
SearchDropdown.SearchResultV = Ember.View.extend(Ember.Metamorph, {
classNameBindings: ['isSelected'],
click: function(){
var content = this.get('content');
SearchDropdown.SelectedSearchController.set('content', content);
var loadcontent = this.get('content');
loadcontent.set("searchRadius", $("select[name=radius]").val());
SearchDropdown.LoadMap.load(content);
},
isSelected: function () {
var selectedItem = SearchDropdown.SelectedSearchController.get('content'),
content = this.get('content');
if (content === selectedItem) {
return true;
}
}.property('SearchDropdown.SelectedSearchController.content')
});
Controller:
SearchDropdown.SelectedSearchController = Ember.Object.create({
content: null,
});
Basically stores the data of the selected view in a controller,