In an Ember Component where should you put reusable code that isn't an action? - ember.js

I have a standard component defined something like:
export default Ember.Component.extend({
hideIfEmptyChange: function() {
var thingOfInterest = ...;
var otherThingOfInterest = ...;
...
// Perform some logic
...
// Perform some logic using thingOfInterest
// Perform exactly the same logic using otherThingOfInterest
}.observes('hideIfEmpty.#each')
});
I want to move the logic for the last two pieces into their own function to prevent writing out the same code twice just for two different variables.
I could write an action on my component and use this.send('myAction',...) - but would it be best practice to have this as an action if it isn't going to be used (or even usable) from the component's template? If you shouldn't do it this way then how should you?
My other thought was mixin's - but the code here will be completely specific to the component, so again this doesn't feel 100% right.
Edit
The component is used here to observe an array of widgets, which are displayed in a sidebar. To start with the sidebar is hidden away as there are no widgets. When a widget is added the sidebar then slides out using a css3 transition between a bootstrap class I added (col-md-0) and something like col-md-2, at the same time the main column shrinks from something like col-md-10 to col-md-8. Because of the nature of it coming out from zero width the widgets inside squish around during the animation and so need to be hidden during it. So the generic piece of code will be:
function(element, classes) {
var lowWidth = 50;
// Transition looks awful if columns start from small width, so hide inner content until css3 transition complete if starting from low width
if ($(element).width() < lowWidth) {
$('div', element).hide();
$(element).toggleClass(classes);
$(element).on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function(e) {
$('div', element).fadeIn(250);
$(this).off(e);
});
}
else {
$(element).toggleClass(classes);
}
}
My component here is in block form and actually has the sidebar within it in the template.
{{#hideable-cols hideIfEmpty=rightSidebarWidgets leftId="det-main" leftMinClass="col-md-8" leftMaxClass="col-md-10" rightId="det-sidebar-right" rightMinClass="col-md-0" rightMaxClass="col-md-2"}}
<div id="det-main" class="col-md-10 resizes">
{{outlet}}
</div>
<div id="det-sidebar-right" class="det-sidebar resizes col-md-0">
{{widget-area widgets=rightSidebarWidgets saveObjectFromWidget="saveObjectFromWidget" removeWidget="removeRightWidget"}}
</div>
{{/hideable-cols}}
The only other way to do what I wanted would have been if I could have set 'inner' components to only be able to react to changes in the array of widgets AFTER the parent block component is happy/done. I've used something similar in knockout js but couldn't see this kind of feature in Ember, so my workaround was to hide the div where the widgets are added, do the transition and show the div again afterward.

Related

How do I set a class on a parent element when a route is the first one loaded?

I have an Ember demo app that works fine if the first route loaded is 'index', 'list' or 'list/index', but not if the first route loaded is 'list/show'. Code is at https://github.com/DougReeder/beta-list , demo is running at https://ember-demo.surge.sh To see the problem, set your window narrower than 640px and surf to https://ember-demo.surge.sh/list/5 You'll see the list panel, rather than the detail panel.
The underlying problem is that, when the route is 'list/show', the divs with class 'panelList' and 'panelDetail' should also have the class 'right'.
I can't set this in the template, because panelList and panelDetail are created by the parent 'list' template. If I move panelList and panelDetail to the child templates 'list/index' and 'list/show', then the list gets re-rendered when going from 'list/index' to 'list/show' which would be a terrible performance hit.
Currently, I use the 'didTransition' action to toggle the class 'right'. This is called both then transitioning from 'list/index' to 'list/show', and when 'list/show' is the initial route. Unfortunately, if 'list/show' is the first route, none of the DOM elements exist when 'didTransition' is called.
I can envision two routes to a solution, but don't know how to implement either:
Toggle the class 'right' on some action which happens after DOM elements exist.
Insert conditional code in the 'list' template, which sets the class 'right' on 'panelList' and 'panelDetail' if the actual route is 'list/show'.
Suggestions?
Answer current as of Ember v2.12.0
You can use the link-to helper to render elements other than links, with styles that change based on the route. Utilizing the activeClass, current-when, and tagName properties, you can basically have that element be styled however you want depending on which route you are on. For example, to render your panelList div:
{{#link-to tagName='div' classNames='panelList' activeClass='right' current-when='list/show'}}
More markup
{{/link-to}}
I love a trick with empty component. In didInsertElement and willDestroyElement hooks you can add and remove a css class from parent element or (I like it better) body. Here is a code:
import Ember from 'ember';
export default Ember.Component.extend({
bodyClass: '',
didInsertElement() {
const bodyClass = this.get('bodyClass');
if (bodyClass) {
Ember.$('body').addClass(bodyClass);
}
},
willDestroyElement() {
const bodyClass = this.get('bodyClass');
if (bodyClass) {
Ember.$('body').removeClass(bodyClass);
}
}
});
I use it in template (in my example it's a template of player route) like this
{{body-class bodyClass='player-page-active'}}
To apply classes to parent element, you can use this.$().parent(), but using body is more reliable. Note that this component will create an empty div, but it shouldn't be a problem (can be in rare cases, fix it with classNames and css if needed).
Sukima suggested looking at currentRouteName, and I thus found hschillig's solution, which I simplified for my case. In the controller, I created an isShow function:
export default Ember.Controller.extend({
routing: Ember.inject.service('-routing'),
isShow: function() {
var currentRouteName = this.get('routing').get('currentRouteName');
return currentRouteName === 'list.show';
}.property('routing.currentRouteName'),
In the template, I now use the if helper:
<div class="panelList {{if isShow 'right'}}">
RustyToms's answer eliminates the need for adding a function to the Controller, at the expense of being less semantic.

Load and unload multiple components in a sidebar using Ember

I have a page where a user is building up an order for a customer. At various stages of the order they may get to a point where they need to add something else. For example, when selecting an address for the customer the customer may have a new address that needs adding, or an existing address may need editing.
I want to be able to load small components on the fly in a right hand sidebar, but there could be any number of them, so I can't just have something like
{{outlet 'right-hand-bar'}}
For example the user may have clicked to add an address, but then may also click a product in the order and I would want to show another component with details on the product, below the add address component. I guess it is kind of like master detail concept, but detail can contain multiple distinct details.
It doesn't feel right to just have a number of outlet's and use the next one available i.e.
{{outlet 'right-hand-bar1'}}
{{outlet 'right-hand-bar2'}}
...
Is there a way to do this in Ember CLI? X number of components in a single outlet?
Edit
I am considering a concept involving creating a sideBar component with a main div
<div class='side-bar-load-area'></div>
and then having a loadComponent action in the sideBar component that takes the name of the component to load.
Not sure how I could load a component by name? I've seen this for a controller:
var comp = App.FooBarComponent.create();
comp.appendTo('#someArea');
and
this.controllerFor('someName');
So ideally would want to combine this and get it working for my sub components?
And also give the sidebar a closeComponent action that a given component could call to unload itself, for an outlet you would do
closeOutlet: function() {
return this.disconnectOutlet({
outlet: 'modal',
parentView: 'application'
});
}
So again just looking to translate this to work for unloading a component in the DOM, not in a outlet?
Assuming you are running Ember 1.11+, you can use the new component helper
Basically, you could do something along the lines of:
templates/main_view.hbs
<div class="sidebar">
{{#each componentNames as |componentName|}}
{{component componentName}}
{{/each}}
</div>
and your buttons to create said components in:
templates/main_view.hbs
<button {{action "addAddress"}}>New address</button>
and the actions themselves in your controller:
controllers/main_view.js
actions: {
addAddress: function() {
var controller = this;
var componentNames = controller.get("componentNames");
componentNames.pushObject("address");
}
}

Appending child view to body / other element

I'm building a popover/dropdown with Ember which essentially boils down to:
<div class="popover">
<span {{action showPopover}}>Click</span>
{{#if popoverShowing}}
<div class="popover-body">The popover body</div>
{{/if}}
</div>
All works fine but I have other elements on the page which are absolutely positioned and due to them forming a new stacking context there's no way I can make the popover be displayed above them.
If this were plain old Javascript, I'd append the popover to the body much like how Bootstrap does with the container option but we don't have that level of control in Ember AFAICT.
The only solution I can think of is to use an {{outlet}} in the application template and render to that, but that means for every popover/dropdown I have to split the contents out to a different view/template/controller and have an action in the router which seems rather over-complicated!
Can anyone think of a better option?
One approach that seems to work is to detach the body element on didInsertElement and then manually destroy on willDestroyElement:
didInsertElement: function() {
Ember.$("body").append(this.$())
},
willDestroyElement: function() {
this.$().remove()
}
This appears to work fine, but there are probably bugs lurking!

binding context to action in ember textfield

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...

Ember Views, Handlebars and jQuery Effects

I would like to incorporate jQuery effects (fadeIn, fadeOut, etc...) in parts of my handlebar templates. I think that this can more or less be accomplished with a separate view in which the view's isVisible property is initially false and its didInsertElement method calls something like this.$().fadeIn().
However, what I'd like to do is add a jQuery effect to just a small part of a view - say for purposes of displaying a small block of content that is initially hidden by an {{#if}} statement that evaluates to false and later through user feedback gets toggled to true. See the following http://jsfiddle.net/YeGbF/2/.
Any suggestions?
You could use a view for the stuff which shall be shown faded in, see http://jsfiddle.net/pangratz666/dJMwC/
Handlebars:
{{#view App.FadeInView contentBinding="this"}}
<div>{{content.someAdditionalDetail}}</div>
{{/view}}
JavaScript:
App.FadeInView = Ember.View.extend({
didInsertElement: function(){
this.$().hide().show('slow');
}
});
Also have a look at Deferring removal of a view so it can be animated