In my ember application I have defined a select element like this in my template:
{{view Ember.Select
content=greetings
optionValuePath="content.id"
optionLabelPath="content.code"
value=selectedGreeting
selection=selectedGreeting
prompt="Please choose"}}
The controller for this page (shared form and controller for create and edit form) looks like this:
greetings: [
{code: "Mr.", id: 1},
{code: "Mrs.", id: 2}
],
selectedGreeting: null,
actions: {
save: function(){
var person = this.get('model');
if (person == null || person.id == undefined)
{
// create mode
var greeting = this.selectedGreeting.id;
// ....
var newPerson = this.get('store').createRecord('person',{
greeting: greeting,
// ..
});
newPerson.save();
this.transitionToRoute('index');
}
else
{
// edit mode
person.set('greeting', this.selectedGreeting.id);
person.save();
}
}
}
This works perfect when creating a new person, but when opening an existing one the select box shows the promt instead of the saved value (do I really have to do this with jQuery?).
Besides I suppose there must be an easier way to update the existing model with the selected values in the else branch, as the other properties get modified automagically.
Any suggestions would be appreciated, the documentation for Ember unfortunately does not help a lot in this case.
The Ember.Select view has quite a few quirks. In fact, if I remember correctly, I saw an issue on Github to just have the whole thing rewritten. For now, I'm assuming that the prompt attribute takes precedence over the selection attribute. To get around that, I would just use a conditional in the template.
{{#if model}}
{{view Ember.Select ... value=selectedGreeting}}
{{else}}
{{view Ember.Select ... prompt='Please Choose'}}
{{/if}}
Also, your second question, if I understand you correctly, you should be able to just use selection=model.greeting to bind the selection value to the model property. And if you're using the template above, you can make it so it only applies to the former and not the latter.
I got it fixed using:
{{view Ember.Select
content=greetings
optionValuePath="content.id"
optionLabelPath="content.code"
value=model.greeting
prompt="Please select"
}}
This works in both ways - loading the data from model and saving the data to the model. The prompt does not interfere with the value from what I experienced.
Related
I am having some trouble trying to get knockout templates to work.
I want to use a select list that allows a person to select a value which in turns shows the template.
The template needs to have its own viewmodel properties which are different between each.
I have created a jsfiddle to show the whole thing
I have 2 very basic templates however when I try running the page I get an error. The code is not production codes its simple throw away stuff so naming conventions are not perfect :)
Error: Unable to process binding "foreach: function (){return contacts }" Message: Unable to process binding "template: function (){return { name:contactTypeId} }" Message: Unknown template type: 1
The template do exist
<script type="text/html" id="1">
<span> Family Template </span>
<input placeholder="From Mum or Dads side"/>
</script>
<script type="text/html" id="2">
<span> Friend Template </span>
<input placeholder="Where did you meet your friend"/>
</script>
I am trying to select the template via a select
<select class="form-control" data-bind="options: $root.contactTypes,
optionsText: 'type',
optionsValue:'id',
value:contactTypeId,
optionsCaption: 'Please Select...'"></select>
2 questions.
Why can it not find the template when I select it from the dropdown?
How would I bind the template to have its own model to allow me to save properties.
Update
Thanks to Georges answer below I have the template binding working. Turns out you can't use an int as an ID for a template without calling to
I have updated my model
self.contactTypeTemplateModel = ko.computed(function () {
return self.contactTypeId === 2 ? someModelWithWhereDidYouMeet : someOtherModel
});
var someModelWithWhereDidYouMeet = {something:ko.observable()};
var someOtherModel = {something:ko.observable()};
It maybe due to no sleep but I can't get this to work. The console is telling me "something is not defined"
Granted my naming is not good. I have also updated the fiddle
The problem for question #1 seems to be that you're passing in a number where it expects a string. For whatever reason, it's not being automatically coerced. This solves it.
template: { name: contactTypeId().toString() }
Even better, create a computed and add a reasonable prefix.
templateName = ko.computed(function() { return "contact-type-" + contactTypeId() })
As for passing in different models. The template binding supports the data property. Your data property can be a computed based on contactTypeId as well.
So you do your template binding with
template: {name: contactTypeTemplateName(), data: contactTypeTemplateModel() }
Where
self.contactTypeTemplateModel = ko.computed(function() {
return self.contactTypeId() === 2 ? someModelWithWhereDidYouMeet
: someOtherModel })
I should also mention, that unless you reuse these templates independently from each other in lots of places, I wouldn't recommend templates for this. I would just use an if binding.
Say I have two objects in my application: 1) Topic, 2) Categories.
Topic has a route /topic/:topic_id
Categories doesn't have a URL route. It contains the list of all available categories that topics can be in.
On the page for Topic, I would like to show a list of all categories, while highlighting the category that the topic is a part of (the 'active' category)
Original Solution
I had originally solved this with a component as follows (a lot of code removed for brevity):
Component Template:
(Goes trough each category and lists them. If the current one is active, it says so.)
<script type="text/x-handlebars" id="components/category-list">
{{#each categories}}
{{#if this.active}} Active -> {{/if}}
{{this.name}}
<br />
{{/each}}
</script>
Component Object:
(Goes through the models via the CategoriesController, marks one category as the current active category for that topic via a "current" parameter passed in from the {{category-list}} component helper)
App.CategoryListComponent = Ember.Component.extend({
tagName: '',
didInsertElement: function () {
var categories = App.CategoriesController;
var current = this.get('current').get('id');
categories.get('content').forEach(function (c) {
if (c.id === current) {
c.active = true;
}
});
this.set('categories', categories);
}.observes('current')
});
Displaying the Category List:
In the Topic view ('category' is the property of Topic that says what category the topic is in):
{{category-list current=category}}
This all works, but I have a feeling that it is not the correct way to go about solving the problem. Components strike me as something that should be very reusable, and this really only is just an encapsulated part of one view.
Second Attempt
Now I am trying to use the category list as a view. So now instead of just a Component, I have a CategoriesRoute (not hooked up in the router though), a CategoriesController, and a CagetoriesView.
When the Topic resource loads up, it sets up the categories controller with the loaded model data:
App.TopicRoute = Ember.Route.extend({
model: function (params) {
return Em.RSVP.hash({
topic: this.store.find('topic', params.topic_id),
categories: this.store.find('category')
});
},
setupController: function (controller, context) {
this._super(controller, context.topic);
this.controllerFor('categories').set('model', context.categories);
}
});
The CategoriesController is just a standard array controller:
App.CategoriesController = Ember.ArrayController.extend({});
The CategoriesView is also very simple:
App.CategoriesView = Ember.View.extend({
templateName: 'categories',
tagName: ''
});
And finally the categories template:
<script type="text/x-handlebars" id="components/category-list">
{{#each categories}}
{{this.name}}
<br />
{{/each}}
</script>
I am rendering the list with the {{render}} helper in the Topic template:
{{render "categories" }}
The Problem:
The above seems to work fine in terms of showing the list of categories. However, I can't figure out how to tell the view which category is active (the category in which the topic is in).
I looked around on the internet, and it looks like the {{render}} helper used to allow for an optional options hash (which is how I solved the problem with a Component in my first attempt), but it seems to have been removed in some recent version of Ember.
I found a StackOverflow entry that mentioned making my own {{render}} helper, but it looks like that is to dynamically change models, or something of that nature. I'd like to keep the models backing for the categories view, I just need to be able to tell it which category is active via the Topic.category property.
Or, was my first attempt the better solution? I am very new to Ember, so I'm not sure what would be the best strategy here. My instinct tells me I should use my second attempt rather than the first, but I am not positive.
Any help is very appreciated.
You are right in saying that components must be re-usable everywhere and should not be tied to any particular controller. I would use a view for this. The way I would do is, I would have a
App.CategoriesController = Em.ArrayController.extend({
itemController: 'category',
//some other stuff here
});
for categories and then have an itemController called
App.CategoryController = Em.ObjectController.extend({
setActiveState: function() {
if (this.get('parentController.current.id') === this.get('content.id')) {
this.set('active', true);
}
}
});
and in the template, you could say
{{#each category in categories}}
{{category.name}}
{{/each}}
I created this in a the company controller.
$scope.$watch('companyName', function () {
console.log($scope.company.name);
});
I wanted to get the company name, how do I call it where the ng-controller matches the controller name in the view?
Would it be something along the lines of this?
<span ng-init="companyName"> {{ companyName() }} </span>
I'm not sure I understand how to get the information from that function in the view. I think I've confused myself.
Yes, your sample seems a bit strange.
Anyway, to bind a $scope property in the view, you use its name.
So in the controller:
$scope.company = { name: "Acme" };
And in the view:
<span> {{ company.name }} </span>
There is no need to create a $watch manually. Angular will create a watch for any property used in the view (for you).
You also, don't have to use ng-init, unless you want to initialize something in the view.
To see the watch in action, simple add a text input next to the span:
<input type="text" ng-model="company.name"/>
If you change the value in the text box, you will see the changes reflected in the span.
Here is a plunker to play with that demonstrates.
I think you have confused yourself!
In the company controller all you need to do is:
$scope.companyName = "ACME";
To access it in the view:
{{ companyName }}
When binding to values, it is assumed to be relative to $scope. In this case (assuming the value you want is the same as what you specify in your $watch), you want to init an item to an object, and it should look something like this:
<span ng-init="company = {name: 'whatever' }">{{company.name}}</span>
For various reasons, it's not recommended to use ng-init except in things like ng-repeat. If it works, it works, but I'd recommend getting used to initializing things in your controller (just add the statement, such as $scope.company = {name: 'whatever'}; somewhere before your controller function ends). One reason is to make it easier to test your controller in unit tests, because it's annoying to make your tests dependent on a particular view).
The simplest was to do it:
controller:
$scope.companyName = "My company";
view:
<span>{{companyName}}</span>
$scope.$watch is just a function to monitor changes in the value of CompanyName, I don't think you need it.
i would like to add some cards to a board, all cards are very different. so i want to create different view for each card that can bind different events and template. I set a 'type' property in the card model which to distinguish the cards.
the board template look like below:
{{#each card in cards}}
{{render card.type card class="card"}}
{{/each}}
However, the first argument for the render help can not be a variable, it can only be the card view name string.
anyone know how to achieve this?
As you already mentioned the built-in render helper only accepts a string to lookup the template to render, therefore one possible solution would be to write your own custom render helper, which then after getting the correct name with card.get('type') delegates the rendering to the built-in render helper. This could look something like this:
Ember.Handlebars.registerBoundHelper('renderCard', function(context, card, options) {
return Ember.Handlebars.helpers.render.call(context, card.get('type'), 'card', options);
});
After that, you could use it like this in your template:
{{#each card in cards}}
{{renderCard this card}}
{{/each}}
Edit
I've also added a basic jsbin that shows it working.
Edit 2
Edited the jsbin again, using object data to be rendered into the template, see here.
Edit 3
Lamentably the DS.FixtureAdapter does not support embedded records which is what you need to make it work. But you could configure you orignal DS.RESTAdapter like this:
App.Adapter = DS.RESTAdapter.extend();
App.Adapter.map('App.Dashboard',
cards: {embedded: 'always'}
);
App.Store = DS.Store.extend({
adapter: App.Adapter
});
This way the card records are always loaded with the parent record. I guess doing this change would make the call to card.get('type') in you handlebar helper return the value rather then undefined.
Hope it helps.
For Ember 1.9 solution above doesn't work. Use another way to register helper:
Ember.Handlebars.registerHelper 'renderDynamic', (context, model, options)->
contextString = options.hash.contextString
model = options.data.view.getStream(model).value()
return Ember.Handlebars.helpers.render(model.get('type'), contextString, options)
And in the template:
= hb 'renderDynamic "this" currentModel contextString="curremtModel"
or in handlebars:
{{renderDynamic "this" currentModel contextString="curremtModel"}}
I am trying to use Ember.select to create a drop down list, my question is when it renders first time, I would like to set a default value. And once I change the selection, it won't check for default value until refresh the page.
Here is the code:
ET.selectedAppYearController = Ember.Object.create({
appYear: null,
isChanged: function () {
if (this.appYear != null) {
LoadETByAppYearETTypeID(this.appYear.text, ET.selectedEmailTypesController.emailType.email_template_type_id);
}
} .observes('appYear'),
setDefault: function() {
if (this.appYear.text == 2012) {
this.set('selection',2012);
}
} .observes('appYear')
});
The View:
{{view Ember.Select
contentBinding = "ET.appYearController.content"
selectionBinding="ET.selectedAppYearController.isDefault"
optionLabelPath="content.text"
optionValuePath="content.value"
}}
I guess i need to set something on selectionBinding...but what kind of value I should bind to?
1. the drop down list values are JSON type.
I was going to make a jsfiddle for this but it seems down at the moment. Will edit one in if it comes back up later as they usually make things clearer.
If you set the view to
{{view Ember.Select
contentBinding = "ET.appYearController"
selectionBinding="ET.selectedAppYearController.appYear"
optionLabelPath="content.text"
optionValuePath="content.value"
}}
and then set up the selectedAppYearController with something like:
ET.selectedAppYearController = Ember.Object.create({
appYear: ET.appYearController.get('lastObject')
}
then you should have the last element in your appYearController set as default, and when you change the selection ET.selectedAppYearController.appYear will reflect this.
Obviously if you want something other than the last element of your appYearController then you can set the value of appYear to whatever you want.
I'm way late to this party, but in the latest version of ember 0.9.7.1 there is a "prompt" attribute you can set in your Ember.Select. It looks something like,
{{view Ember.Select
prompt="This is selected by default"}}
Hope that helps some one.
It's hard to tell from your code example. One thing you could do is extract all your domain specific requirements and set up a live jsfiddle example that tries to accomplish what you are trying to do in a more generic way. http://jsfiddle.net/ You can see examples of using jsfiddle in other emberjs posts here.
But one thing I remarked is that your selectionBinding appears to be wrong. This should bind to the value you are trying to set, not a default. You could set a default in the controller if you like (just by assigning it to the value bound by selectionBinding). So I think your selectionBinding should be "ET.selectedAppYearController.appYear" if I understand your example correctly.
Not sure if anyone still needs that, but the way I did it is binding to the value with something like
{{view Ember.Select content=templates optionValuePath="content.id" optionLabelPath="content.name" value=selectedTemplateId class="form-control"}}
then in controller implement selectedTemplateId as computed property, working both as setter and getter:
selectedTemplateId: ( (key, value)->
# setter code:
if arguments.length > 1
#set('selTemplateId', value)
return value
# getter code
else
if #get('selTemplateId')?
return #get('selTemplateId')
else
return #get('templates')?.objectAt(0).id
).property()
Sorry for CoffeeScript instead of js. JS version at: http://goo.gl/5FZHFh
A bit of docs for computed properties: http://emberjs.com/api/classes/Ember.ComputedProperty.html