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. :)
Related
I have a template with the following code:
{{#each types itemController='type'}}
<div class='col checkbox'>
<label>
{{input type='checkbox' checked=isSelected disabled=notAllowed}}
<span {{bind-attr class='isSelected'}}>{{name}}</span>
</label>
</div>
{{/each}}
types is set in setupController:
this.store.find('type').then(function(types){
controller.set('types', types);
});`
//Having 2 other models here that I am setting and having an itemController for, exactly in the same fashion as types.
for the ArrayController which has the itemController.
NOTE: To clarify, I am using and setting 3 different models, which work pretty much in the same way as type, that makes this a bit more complicated.
Then the itemController itself:
App.TagController = Ember.ObjectController.extend({
isSelected: function(key, value){
//bunch of code that does some stuff and returns true or false depending on value
}.property()
});
App.TypeController = App.TagController.extend();
Now the problem: I have a resetbutton that should deselect all checkboxes and remove the span classes.
I would have thought about using an action (in the ArrayController) that sets all the isSelected properties to false, but I don't seem to be able to find a way to access and manually set that itemController computed property.
One thing I tried in the ArrayController is the following:
actions: {
resetFilters: function(){
this.get('types').forEach(function(type) {
console.log(type.get('isSelected'));
//type.set('isSelected', false);
});
}
}
But unfortunately this returns undefined. And using jQuery manually to remove the class and uncheck the checkbox seems to work the first instance, but the problem is, the computed property doesn't get updated and that messes things up.
Any idea how I can achieve what I want?
If anything is unclear let me know and I will do my best to clarify.
Thank you.
You are setting controller.types, this will not work with itemController. You should always be setting an array controller's content property.
The following should work:
controller.set('content', this.store.find('type'));
Then to set the isSelected:
controller.setEach('isSelected', false);
This assumes that controller is an instance of an ArrayController that has an itemController set in it's definition, e.g.
App.TypesController = Em.ArrayController.extend({itemController: 'type'});
store.find returns a PromiseArray, so it should be resolved first. You can set the types as follows in setupController:
this.store.find('type').then(function(types){
controller.set('types', types);
});
Or you can resolve types in the reset:
this.get('types').then(function(types) {
types.forEach(function(type) {
console.log(type.get('isSelected'));
});
});
I would recommend the first one though.
Apparently this does not work: http://jsbin.com/efapob/3/edit
Ember.Handlebars.registerHelper('foo', function(options) {
var result = 'BEFORE '
+ options.fn(this)
+ ' AFTER';
return new Handlebars.SafeString(result);
});
And I assume it's because the fn() writes directly to the output buffer.
However, I need a way to directly work with the output of the block's content.
I tried overwriting a view's render function, but that also didn't lead me anywhere.
(Background: I'm trying to write an {{#ifchanged}} helper block that only renders if the contents have changed in comparison to the last call. The use case is a loop that should display something every time one property of the model is different to the last one. If you have other ideas how to achieve this, comments very appreciated!)
If anyone is interested, in this specific use-case I worked around the issue of not being able to use the return of fn() like so:
var ifchanged_last;
Ember.Handlebars.registerHelper('ifchanged', function(property, options) {
var value = Ember.Handlebars.get(this, property);
if (value !== ifchanged_last) {
options.fn(this, options);
}
ifchanged_last = value;
return;
});
Template:
{{#each content}}
{{#ifchanged some_key}}
The value changed
{{/ifchanged}}
{{/each}}
Large room for improvement, but a usable starting point.
You can use isDirty property from DS.Model to know when the data changes.
In some template:
{{#if isDirty}}
You changed the model<br/>
{{/if}}
And a jsfiddle with the demo.
I would like to change emberjs's onEvent which is the trigger to perform the associated view action. There seems to be just 2 options for onEvent: the default enter, and keypress. I'd like to know if I can have other options as well, such as focusOut.
Small question
But first, I couldn't even get the non-default option to work:
Myapp.TextField = Ember.TextField.extend({
onEvent: 'keypress'
});
The text field didn't respond to key presses, but continued to respond to enter.
Actual question
How can we let ember.js text field respond to other onEvents to trigger the action specified in the view. This is something I'm expecting:
HBS:
<script type="text/x-handlebars" id="themodel">
{{view Myapp.TextField action="targetAction" valueBinding="myText"}}
</script>
JS view:
Myapp.TextField = Ember.TextField.extend({
// is this possible?
onEvent: 'focusOut'
});
JS controller:
Myapp.ThemodelController = Ember.ArrayController.extend({
targetAction: function(){
var usertext = this.get('myText');
// do stuff with the usertext ...
}
});
A workaround solution for the "Actual question"
This is a work-around as it doesn't modify onEvent, but directly lets focusOut trigger the targetAction:
JS view:
Myapp.TextField = Ember.TextField.extend({
focusOut: function(){
this.get('controller').set('myText', this.get('value')).targetAction();
}
});
But I really don't like this cumbersome implementation, so please let me know if there is a way to utilize onEvent with focusOut. Thanks.
I experienced something similar to your "small problem", only instead of the text field only responding to enter, it would respond to neither keypresses nor enter.
The solution lies within mavilein's answer to this question - the word "keypress" must be in camelCase, i.e "keyPress". This applies whether it is used as a property name when extending the Ember.TextField class, as you had originally attempted, or as an attribute on the actual view element as Mike had suggested. I tried it both ways. Hope this helps.
But first, I couldn't even get the non-default option to work
Strange. There is a test for exactly this behavior. Suggest having a look and see what's different in your use case.
How can we let ember.js text field respond to other onEvents to trigger the action specified in the view.
Following the same pattern used in Ember.TextField's insertNewLine fx
Myapp.TextField = Ember.TextField.extend({
onEvent: 'focusOut',
focusOut: function() {
sendAction('focusOut', this, event);
}
});
I've got an ember application that needs to manage multiple chat windows. A window for each active chat is created within an {{#each}} loop. This is straightforward enough. The place that I'm having trouble is sending the chat message when the user presses enter.
The window looks like this
{{#each chats}}
... stuff to display already existing chats...
{{view Ember.TextField valueBinding="text" action="sendChat"}}
<button {{action sendChat this}}> Send </button>
{{/each}}
This works fine for the button, since I can pass this to it. By default the function defined in the textfield view action just gets the text within that textfield, which is not enough in this case. Since there can be multiple chat windows open, I need to know which window the message was typed into. Is it possible to pass this to the textfield action function? (or can you suggest a different way to solve this problem?)
Add contentBinding="this" to the definition of the view, like:
{{view Ember.TextField valueBinding="text" action=sendChat contentBinding="this"}}
EDIT
Ember master already has this change, but the official downloadable verstion still don't.. so you will need to subclass the Ember.TextField and change its insertNewline to achieve required functionality:
App.ActionTextField = Em.TextField.extend({
insertNewline: function(event) {
var controller = this.get('controller'),
action = this.get('action');
if (action) {
controller.send(action, this.get('value'), this);
if (!this.get('bubbles')) {
event.stopPropagation();
}
}
}
});
After that, the action handler will receive additional argument, the view:
{{view App.ActionTextField valueBinding="text" action=sendChat myfieldBinding="this"}}
and in controller:
sendChat: function (text, view) {
var myField = view.get('myfield');
//do stuff with my field
}
You may use ember master instead of subclassing Ember.TextField..
I hope the ember guys will release the next version soon..
I know this question has been answered but I said let me add some information that may help out someone in the situation of actions and TextField. One word "Component". TextField in Ember is a Component so if you think of TextField from that perspective it may help when it comes to sending actions and using TextField in an application.
So when you say App.SomeTextField = Ember.TexField.extend({...});App.SomeTextField is subclassing Ember.TextField (remember which is a component). You could add your logic inside and that works and you could access it from your template such as {{view App.SomeTextField}}
You may be thinking I see the word 'view' this guy sucks, TextField is a View. Well, it is sort of a View because Ember Components are a subclass of Ember.View so they have all that Views have. But there are some important things to keep in mind Components un-like Views do not absorb their surrounding context(information/data), they lock out everything and if you want to send something from the outside surrounding context you must explicitly do so.
So to pass things into App.SomeTextField in your template where you have it you would do something like {{view App.SomeTextField value=foo action="sendChat"}} where you are passing in two things value, and action in this case. You may be able to ride the fine line between View/Component for a bit but things come crashing why is your action not sending?
Now this is where things get a little trippy. Remember TextField is a Component which is subclassed from View but a View is not a Component. Since Components are their own encapsulated element when you are trying to do this.get('controller').send('someAction', someParam), "this" is referring to the Component its self, and the controller is once again the component its self in regards to this code. The action that you are hoping will go to the outside surrounding context and your application will not.
In order to fix this you have to follow the protocol for sending actions from a Component. It would be something like
App.SomeTextField = Ember.TextField.extend({
//this will fire when enter is pressed
insertNewline: function() {
//this is how you send actions from components
//we passed sendChat action in
//Your logic......then send...
this.sendAction('sendChat');
}
});
Now in the controller that is associated with where your SomeTextField component/view element is you would do
App.SomeController = Ember.Controller.extend({
//In actions hash capture action sent from SomeTextField component/view element
actions: {
sendChat: function() {
//Your logic well go here...
}
}
});
Now I said to think of TextField as a Component but I have been riding the tail of the view and declaring {{view AppSomeTextField...}}. Lets do it like a component.
So you would have in your template where you want to use it
//inside some template
`{{some-text-field}}`
Then you get a specfic template for the component with the name:
//template associated with component
<script type="text/x-handlebars" data-template-name="components/some-text-field">
Add what you want
</script>
In your JS declare your component:
//important word 'Component' must be at end
App.SomeTextFieldComponent = Ember.TextField.extend({
//same stuff as above example
});
Since we on a role you could probably get the same functionality using Ember input helpers. They are pretty powerful.
{{input action="sendChat" onEvent="enter"}}
Welp hopefully this information will help someone if they get stuck wondering why is my action not sending from this textField.
This jsBin is a sandBox for Components/Views sending actions etc....Nothing too fancy but it may help someone..
http://emberjs.jsbin.com/suwaqobo/3/
Peace, Im off this...
I have a view that has expandable/collapsable content that I'd like to be able to toggle by clicking the on the table row. Before pre1.0, I had this in the template:
<tr {{action "expand"}}>
which was previously handled on my view:
App.ContentRowView = Em.View.extend({
templateName: 'ember/templates/content/row',
expand: function() {
this.set('isExpanded', !this.get('isExpanded'));
},
isExpanded: false
});
However, after upgrading to pre1.0 the action is now fielded directly by the router. This makes sense in a lot of situations, but in this case the expansion is really a view concern. I've tried just replacing this with a click event handler without luck.
Is there a best practice on how to handle a view concern event like this with pre1.0?
Deprecated Answer
Even if the answer of #outside2344 works, I think it's not exactly right.
Indeed parentView does not represent the view, but the parentView of its parentView.
Since 1.0-pre, views preserve their context, so in the template, this represents the parentView, parentView represents parentView.parentView, and view represents the current view.
Here is a fiddle to illustrate this: http://jsfiddle.net/Sly7/cnmJa/
For me the answer is {{action expand target="view"}}
EDIT (answering to #Gal Ben-Haim)
Action helpers behave little different in a router-based application. Quote from the documentation:
In Router-driven applications, if an action is not intercepted by a view, that event will bubble up to the Route in which that view was rendered. If that Route is a sub-route of another Route the transition will be sought there all the way up to the top-level Route definition, our über-container: root.
This bubbling effect allows certain actions to remain private. If certain transitions should only be available for certain sub-sub-states, put the transition on the sub-state and you've achieved a type of scoping.
Basically, for me that means in Router-driven apps if you don't explicitly define a target in the action helper, it is sent to the router.
Updated answer
I think now the guides answer very well to this question. see http://emberjs.com/guides/templates/actions/#toc_specifying-a-target
In pre1.0 you can make the view field the action by adding target="parentView" to the action:
{{action "expand" target="parentView"}}
Events doesn't bubble through the view-hierarchy by default. You can change this (though I can't say I'd recommend it):
(function() {
Ember.View.reopen({
// Let actions bubble to parentView by default.
target: function() {
return this.get('parentView');
}.property('parentView')
});
})();