In my app I have a generic text field of type Em.TextField:
App.DetailTextField = Em.TextField.extend({
attributeBindings: ['required', 'readonly', 'name']
});
In my template I use the DetailTextField to show data, to specify an attribute, and to show it in the class of either 'editing' or 'viewing':
{{view App.DetailTextField viewName="tbSurname" placeholder="surname"
valueBinding="surname" required="required" classNameBindings="isEditing:editing:viewing" readonlyBinding='getReadOnlyState'}}
This works fine but I have several of these fields, all of which have the same part: classNameBindings="isEditing:editing:viewing" readonlyBinding='getReadOnlyState'. isEditing and getReadOnlyState are retrieved from the current objectController of the template view.
Is there a way to put the classNameBindings and readonlyBinding into the DetailTextField class definition, so that it does not need to be explicitely typed into every instance of the DetailTextField view? That is, can DetailTextField get the current context - e.g:
App.DetailTextField = Em.TextField.extend({
attributeBindings: ['required', 'readonly', 'name'],
classNameBindings: "this.view.get('isEditing'):editing:viewing"
});
I could make isEditing a function within the class definition that retrieved the value from the controller, but I still have the same problem in that I would not know how to reference the activeController / this.controller.
Any thoughts?
To access controller from your view, simply
mycontroller = this.get('controller');
Btw, I prefer to put isEditing attribute in my model, so I don't have to extend views.
App.MyModel.reopen({
isEditing: false
});
So I can loop each of them...
{{#each model}}
{{#if isEditing}}
{{view Em.TextField valueBinding="yourvaluebindinghere"}}
<button {{action 'update' this}}>Update</button>
{{else}}
{{yourvaluebindinghere}}
<button {{action 'edit' this}}>Edit</button>
{{/if}}
{{/each}}
And put the actions in my controller:
App.MyController = Em.ArrayController.extend({
actions: {
edit: function(model) {
model.set('isEditing', true);
},
update: function(model) {
model.save();
model.set('isEditing', false);
}
}
});
Related
I'm attempting to make an admin backend for my Rails app with Ember.
Here's a JsBin illustrating the problems I'm having.
http://emberjs.jsbin.com/titix/20/edit
In short, I want to be able to edit the title of a arbitrary model inside of a list of other models when a user clicks on it.
Relevant CoffeeScript with questions in the comments:
App.ItemView = Ember.View.extend
templateName: "item"
isEditing: false
didInsertElement: ->
# 1. Is there a better way to toggle the isEditing property when the title is clicked?
view = #
#$('.title').click ->
view.toggleProperty('isEditing')
# 2. How would I unset isEditing when the user clicks on a different App.ItemView?
# 3. How do I set App.ItemController to be the controller for App.ItemView?
App.ItemController = Ember.Controller.extend
# 4. How would I then toggle the isEditing property of App.ItemView on either save of cancel from App.ItemController?
actions:
save: ->
# set isEditing is false on App.ItemView
#get('model').save()
cancel: ->
# set isEditing is false on App.ItemView
#get('model').rollback()
Any help on any of these questions would be appreciated.
Okay, let's see if I can remember to answer all of the questions.
Firstly we decide to wrap the entire set of items in an array controller (this allows us to keep track of all of the children item controllers). It also allows us to define an itemController which the items can use.
<script type="text/x-handlebars" data-template-name="item-list">
<h3>{{view.title}}</h3>
<ul>
{{render 'items' view.content}}
</ul>
</script>
App.ItemsController = Em.ArrayController.extend({
itemController:'item',
resetChildren: function(){
this.forEach(function(item){
item.set('isEditing', false);
});
}
});
Secondly the render template is defined ({{render 'items' view.content}} will render the items template)
<script type="text/x-handlebars" data-template-name="items">
{{#each item in controller}}
<li>{{view App.ItemView content=item}}</li>
{{/each}}
</script>
Thirdly since we iterated over the controller it will use this modified item controller
App.ItemController = Ember.ObjectController.extend({
isEditing: false,
isSaving: false,
actions: {
startEditing: function(){
this.parentController.resetChildren();
this.set('isEditing', true);
},
save: function() {
var self = this;
this.set('isEditing', false);
this.set('isSaving', true);
this.get('model').save().finally(function(){
//pretend like this took time...
Em.run.later(function(){
self.set('isSaving', false);
}, 1000);
});
},
cancel: function() {
this.set('isEditing', false);
this.get('model').rollback();
}
}
});
and here's our template
<script type="text/x-handlebars" data-template-name="item">
{{#if controller.isEditing}}
{{input value=controller.title }}
<button {{ action 'cancel' }}>Cancel</button>
<button {{ action 'save' }}>Save</button>
{{else}}
<div {{action 'startEditing'}}>
<div class="title">{{controller.title}}</div>
</div>
{{/if}}
{{#if controller.isSaving}}
Saving...
{{/if}}
</script>
Example: http://emberjs.jsbin.com/jegipe/1/edit
Here is a working bin toggles the state of the form item in the following conditions, save button click, cancel button click and click on an another item.
Every time we click on an item, I save the item views reference to the index controller. When an other item is clicked, I use the a beforeObserver to set the previous item views state to false.
I also specified the item controller in the template.
App.IndexController = Em.ObjectController.extend({
currentEditingItem: null,
currentEditingItemWillChange: function() {
if(this.get('currentEditingItem')) {
this.set('currentEditingItem.isEditing', false);
}
}.observesBefore('currentEditingItem'),
});
App.ItemController = Ember.Controller.extend({
needs: ['index'],
formController: Em.computed.alias('controllers.index'),
currentEditingItem: Em.computed.alias('formController.currentEditingItem'),
actions: {
save: function() {
this.set('currentEditingItem.isEditing', false);
return this.get('model').save();
},
cancel: function() {
this.set('currentEditingItem.isEditing', false);
return this.get('model').rollback();
}
}
});
See http://jsfiddle.net/4ZyBM/6/
I want to use Bootstrap for my UI elements and I am now trying to convert certain elements to Ember views. I have the following problem:
I embed an input element in a DIV with a given class (control-group). If a validation error occurs on the field, then I want to add an extra class "error" to the DIV.
I can create a view based on the Ember.TextField and specify that if the error occurs the ClassNameBinding should be "error", but the problem is that class is the set to the input element and not to the DIV.
You can test this by entering a non alpha numeric character in the field. I would like to see the DIV border in red and not the input field border.
HTML:
<script type="text/x-handlebars">
<div class="control-group">
{{view App.AlphaNumField valueBinding="value" type="text" classNames="inputField"}}
</div>
</script>
JS:
App.AlphaNumField = Ember.TextField.extend({
isValid: function () {
return /^[a-z0-9]+$/i.test(this.get('value'));
}.property('value'),
classNameBindings: 'isValid::error'
})
Can I set the classNameBindings on the parent element or the element closest to the input ? In jQUery I would use:
$(element).closest('.control-group').addClass('error');
The thing here is that without using jQuery you cannot access easily the wrapping div around you Ember.TextField's. Also worth mentioning is that there might be also a hundred ways of doing this, but the simplest solution I can think of would be to create a simple Ember.View as a wrapper and check the underlying child views for validity.
Template
{{#view App.ControlGroupView}}
{{view App.AlphaNumField
valueBinding="value"
type="text"
classNames="inputField"
placeholder="Alpha num value"}}
{{/view}}
Javascript
App.ControlGroupView = Ember.View.extend({
classNameBindings: 'isValid:control-group:control-group-error',
isValid: function () {
var validFields = this.get('childViews').filterProperty('isValid', true);
var valid = validFields.get('length');
var total = this.get('childViews').get('length')
return (valid === total);
}.property('childViews.#each.isValid')
});
App.AlphaNumField = Ember.TextField.extend({
isValid: function () {
return /^[a-z0-9]+$/i.test(this.get('value'));
}.property('value')
});
CSS
.control-group-error {
border:1px solid red;
padding:5px;
}
.control-group {
border:1px solid green;
padding:5px;
}
Working demo.
Regarding bootstrap-ember integration and for the sake of DRY your could also checkout this ember-addon: https://github.com/emberjs-addons/ember-bootstrap
Hope it helps.
I think that this is the more flexible way to do this:
Javascript
Boostrap = Ember.Namespace.create();
To simplify the things each FormControl have the properties: label, message and an intern control. So you can extend it and specify what control you want. Like combobox, radio button etc.
Boostrap.FormControl = Ember.View.extend({
classNames: ['form-group'],
classNameBindings: ['hasError'],
template: Ember.Handlebars.compile('\
<label class="col-lg-2 control-label">{{view.label}}</label>\
<div class="col-lg-10">\
{{view view.control}}\
<span class="help-block">{{view.message}}</span>\
</div>'),
control: Ember.required()
});
The Boostrap.TextField is one of the implementations, and your component is a Ember.TextField. Because that Boostrap.TextField is an instance of Ember.View and not an Ember.TextField directly. We delegate the value using Ember.computed.alias, so you can use valueBinding in the templates.
Boostrap.TextField = Boostrap.FormControl.extend({
control: Ember.TextField.extend({
classNames: ['form-control'],
value: Ember.computed.alias('parentView.value')
})
});
Nothing special here, just create the defaults values tagName=form and classNames=form-horizontal, for not remember every time.
Boostrap.Form = Ember.View.extend({
tagName: 'form',
classNames: ['form-horizontal']
});
Create a subclass of Boostrap.Form and delegate the validation to controller, since it have to be the knowledge about validation.
App.LoginFormView = Boostrap.Form.extend({
submit: function() {
debugger;
if (this.get('controller').validate()) {
alert('ok');
}
return false;
}
});
Here is where the validation logic and handling is performed. All using bindings without the need of touch the dom.
App.IndexController = Ember.ObjectController.extend({
value: null,
message: null,
hasError: Ember.computed.bool('message'),
validate: function() {
this.set('message', '');
var valid = true;
if (!/^[a-z0-9]+$/i.test(this.get('value'))) {
this.set('message', 'Just numbers or alphabetic letters are allowed');
valid = false;
}
return valid;
}
});
Templates
<script type="text/x-handlebars" data-template-name="index">
{{#view App.LoginFormView}}
{{view Boostrap.TextField valueBinding="value"
label="Alpha numeric"
messageBinding="message"
hasErrorBinding="hasError"}}
<button type="submit" class="btn btn-default">Submit</button>
{{/view}}
</script>
Here a live demo
Update
Like #intuitivepixel have said, ember-boostrap have this implemented. So consider my sample if you don't want to have a dependency in ember-boostrap.
In my application I display a list of accounts like so:
<script type="text/x-handlebars" data-template-name="accounts">
{{#each account in controller}}
{{#linkTo "account" account class="item-account"}}
<div>
<p>{{account.name}}</p>
<p>#{{account.username}}</p>
<i class="settings" {{ action "openPanel" account }}></i>
</div>
{{/linkTo}}
{{/each}}
</script>
Each account has a button which allows users to open a settings panel containing settings just for that account. as you can see in this quick screencast:
http://screencast.com/t/tDlyMud7Yb7e
I'm currently triggering the opening of the panel from within a method located on the AccountsController:
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
console.log('trigger the panel');
}
});
But I feel that it's more appropriate to open the panel from within a View that I've defined for this purpose. This would give me access to the View so that I can perform manipulations on the DOM contained within it.
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed'],
templateName: 'mainPanel',
openPanel: function(){
console.log('opening the panel');
}
});
<script type="text/x-handlebars" data-template-name="mainPanel">
<div id="panel-account-settings" class="panel closed">
<div class="panel-inner">
<i class="icon-cancel"></i>close
<h3>Account Settings</h3>
Disconnect Account
</div>
</div>
</script>
The problem I'm encountering is that I don't see how I can trigger a method on the Social.MainPanelView from the context of the AccountsController. Is there a better solution?
UPDATE 1
I've worked up a Fiddle to illustrate what I'm talking about:
http://jsfiddle.net/UCN6m/
You can see that when you click the button it calls the showPanel method found on App.IndexController. But I want to be able to call the showPanel method found on App.SomeView instead.
Update:
Approach One:
Simplest of all
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
/* we can get the instance of a view, given it's id using Ember.View.views Hash
once we get the view instance we can call the required method as follows
*/
Ember.View.views['panel-account-settings'].openPanel();
}
});
Fiddle
Approach Two:(Associating a controller, Much Cleaner)
Using the Handlebars render helper: what this helper does is it associates a controller to the view to be displayed, so that we can handle all our logic related to the view in this controller, The difference is
{{partial "myPartial"}}
just renders the view, while
{{render "myPartial"}}
associates App.MyPartialController for the rendered view besides rendering the view, Fiddle
now you can update your code as follows
application.handlebars(The place you want to render the view)
{{render "mainPanel"}}
accounts controller
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(account){
this.controllerFor("mainPanel").openPanel();
}
});
main panel view
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed']
});
main panel controller
Social.MainPanelController = Ember.Controller.extend({
openPanel: function(){
console.log('opening the panel');
}
})
Approach Three:
This one is the manual way of accomplishing Approach Two
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
controllerBinding: 'Social.MainPanelController',
classNames: ['panel', 'closed'],
templateName: 'mainPanel'
});
Social.MainPanelController = Ember.Controller.extend({
openPanel: function(){
console.log('opening the panel');
}
})
use this.controllerFor("mainPanel").openPanel()
You need to use the action helper rather than directly coding the links. The action helper targets the controller by default, but you can change it to target the view instead:
<a {{action openPanel target="view"}}></a>
Your second link should be a linkTo a route, since you are specifying a link to another resource. The whole snippet, revised:
Social.MainPanelView = Ember.View.extend({
id: 'panel-account-settings',
classNames: ['panel', 'closed'],
templateName: 'mainPanel',
openPanel: function(){
console.log('opening the panel');
}
});
<script type="text/x-handlebars" data-template-name="mainPanel">
<div id="panel-account-settings" class="panel closed">
<div class="panel-inner">
<a {{action openPanel target="view"} class="button button-close"><i class="icon-cancel"></a></i>
<h3>Account Settings</h3>
{{#linkTo "connections"}}Disconnect Account{{/linkTo}}
</div>
</div>
</script>
I have this code for view:
App.TodoView = Em.View.extend({
labelView: Em.TextField.extend({
}),
createNew:function () {
console.log(this.labelView.get('value'));
}
});
and this template:
{{#view App.TodoView}}
{{view labelView}}
{{#view Em.Button target="parentView" action="createNew"}}Add{{/view}}
{{/view}}
And I get the following error:
Uncaught TypeError: Object (subclass of Ember.TextField) has no method 'get'
I want to use insertNewLine method too, so I can set the value of the Em.TextField in template.
The problem is that you are defining a class and trying to get the value from it. What you rather want is to get the value of a concrete instance. This can be achieved by binding the value of the LabelView to a value which can then be retrieved in the App.TodoView, in this case todoLabel, see http://jsfiddle.net/pangratz666/PTPsV/:
Handlebars:
{{#view App.TodoView }}
<!-- Bind the value of the LabelView to todoLabel on the App.TodoView -->
{{view LabelView valueBinding="todoLabel" }}
{{#view Em.Button target="parentView" action="createNew" }}Add{{/view}}
{{/view}}
JavaScript:
App.TodoView = Em.View.extend({
LabelView: Em.TextField.extend(),
createNew: function(){
var value = this.get('todoLabel');
console.log( 'le todoLabel', value );
}
});
Note that since you are defining a class LabelView it's a convention to write it in Uppercase, whereas instances are written in lowerCase. See a good blog post about naming convention by The Emberist.
Also, to access a property on an Ember.Object, you should always use get, so it's this.get('todoLabel') and not this.todoLabel.
You can now implement further methods like insertNewline and cancel - note it's insertNewline and not insertNewLine, see text_support.
The result would look like this, see http://jsfiddle.net/pangratz666/9ZLAC/:
App.TodoView = Em.View.extend({
LabelView: Em.TextField.extend({
insertNewline: function(){
this.get('parentView').createNew();
},
cancel: function(){
this.set('value', '');
}
}),
createNew: function(){
var value = this.get('todoLabel');
console.log( 'le todoLabel', value );
}
});
Consider a View that defines a list of objects:
App.ListView = Ember.View({
items: 'App.FooController.content'
itemClicked: function(item){
}
)};
with the template:
<ul>
{{#each items}}
{{#view App.ItemView itemBinding="this" tagName="li"}}
<!-- ... -->
{{/view}}
{{/each}}
</ul>
and the ItemView:
App.ItemView = Ember.View.extend({
click: function(event){
var item = this.get('item');
// I want to call function itemClicked(item) of parentView
// so that it handles the click event
}
})
So basically my question is how do I pass events to parent views, especially in the case where the parent view is not known by the child view? I understand that you can get a property foo of a parentView with either this.getPath('parentView').get('foo') or this.getPath('contentView').get('foo'). But what about a function (in this case, itemclicked())?
this.get('parentView').itemClicked(this.get('item')); should do the trick.
You can use the {{action}} helper, see: http://jsfiddle.net/smvv5/
Template:
<script type="text/x-handlebars" >
{{#view App.ListsView}}
{{#each items}}
{{#view App.ListView itemBinding="this" }}
<li {{action "clicked" target="parentView" }} >{{item.text}}</li>
{{/view}}
{{/each}}
{{/view}}
</script>
JS:
App = Ember.Application.create({});
App.Foo = Ember.ArrayProxy.create({
content: [Ember.Object.create({
text: 'hello'
}), Ember.Object.create({
text: 'action'
}), Ember.Object.create({
text: 'world'
})]
});
App.ListsView = Ember.View.extend({
itemsBinding: 'App.Foo',
clicked: function(view, event, ctx) {
console.log(Ember.getPath(ctx, 'item.text'));
}
});
App.ListView = Ember.View.extend({
});
Recent versions of Ember use the actions hash instead of methods directly on the object (though this deprecated method is still supported, it might not be for long). If you want a reference to the view passed to the handler, send through "view" as a parameter and use the parentView as the target.
<button {{action "onClicked" view target="view.parentView"}}>Click me.</button>
App.ListsView = Ember.View.extend({
actions: {
onClicked: function(view) {
}
}
});
{{action}} helper does not send through the event object. Still not sure how to get reference to the event if you need it.
source