Can an Ember component observe a controller property? - ember.js

I have a controller and a component. when the component is rendered, it is passed on in this manner:
{{modal-filter feature=feature parentController=this.controller}}
where feature is a param passed in via controller to handlebars, and parentController is the controller.
Now, in the controller itself, there is an property (an array). let's call that array requiredValues.
Now within a controller/component itself, we can easily set:
valueObserver : function(){
...
}.observes('requiredValues')
However, I need to observe this controller property from a the modal-filter component. So in the modal-filter component, what would I put as the observer function:
valueObserver : function(){
...
}.observes(???)

Passing an entire controller to a component is a massive code smell. It violates the basic principle of component encapsulation. If the "component" is so tightly coupled to the controller, then it's a view, from where you can access the controller by simply saying this.controller. Input to components should be strictly through parameters passed in when they are invoked. Output from components is through send, which the controller can map to some behavior of its choosing in its view's template by saying {{my-component action='eraseHardDisk'}}.
You don't need to directly observe anything on the controller from within the component. If you call the component with {{my-component param=someProperty}}, then any change to the controller's someProperty will automatically be propagated to the param of the component. The component can then define some computed property on param, or observe it, or use it in its own template where it will be automatically kept up-to-date.

What you shouldn't do, but I'll tell you how for completeness
If you're passing the controller in, you can just watch an item on the parentController property, though I wouldn't recommend this at all.
valueObserver : function(){
...
}.observes('parentController.requiredValues')
This would assume the entire array is being replaced, not just an item added, or changed.
Item Added or Removed
valueObserver : function(){
...
}.observes('parentController.requiredValues.[]')
Item Property foo changed on one of the requiredValues items
valueObserver : function(){
...
}.observes('parentController.requiredValues.#each.foo')
What you should do
Instead of passing in the controller, just pass in the property, and observe the property.
{{modal-filter feature=feature property=someProperty}}
propertyObserver : Ember.observer('property', function(){
...
})

Related

Curious behavior when observing Ember's dirtyType property

I try to observe (in my controller) if my Ember model has changed.
personChanged: function() {
// do stuff
}.observes('person.dirtyType'),
This observer is never triggerd unless I will access the isDirty property before. For example if I get the property in the route (where the model is fetched) the observer is triggerd exactly 1 time.
model.people.get('firstObject').get('dirtyType');
controller.set('person', model.people.get('firstObject'));
If I want to get the observer triggered every time the model changed I need to access dirtyType within the observer again.
personChanged: function() {
this.get('person.dirtyType');
// do stuff
}.observes('person.dirtyType'),
The value of dirtyType in the observer is always as expected.
Maybe I'm doing it completely wrong but I can't follow the behavior above.
There is something unpredictable is going on when we use firstObject based on this question Ember computed alias on array firstObject not working
I haven't experienced it to confirm. May be until then you can try the below workaround,
controller.set('person', model.people.get('firstObject'));
Instead of the above, you can define computed property,
person:Ember.computed('model.people.[]',function(){
return return this.get('model.people.firstObject');
})
Now your below observer will work all the time.
personChanged:Ember.observer('person.dirtyType',function() {
// do stuff
}),

emberjs two-way-binding temporarily broken after value assignment at init

I am trying to assign default values for one of my components at init lifecycle hook; in case an undefined value is passed from parent component. Initially everything seems to be working as expected; however when my component is forced a re-render (possibly via another property value update at parent component); undefined value at parent component is written back to my component.
This means; the value assignment I made during initialization is not reflected to parent component, in other words two-way-binding is temporarily not working (the values of parent and child components are not synchronized). Is it the expected behavior or am I missing sth. important about init event? Where is the appropriate place to initialize undefined values of a component? See the twiddle for a simple illustration.
Okay, first a way to work around is to use the update function on the attr. Checkout this twiddle.
I replaced this.set('name', 'tom') with this.attrs.name.update('tom') in the .js and {{name}} with {{attrs.name}} in the .hbs.
Another way to work around is just to wrap the asignment in a Ember.run.later like I've done here, where I replaced this.set('name', 'tom') with that:
Ember.run.later(() => {
this.set('name', 'tom');
});
So that are the workarounds.
The fact that the bindings are not completely set up on init has a long history. But generally its an anti pattern to initialize a value in a child component and give this value up to the parent. It makes your code less readable and is agains the DDAU (Data down, Actions up) principle.
I recommend to initialize the data explicit when you create your models, not implicit during your component evaluation cycle.
For default values that you don't want to store I recommend you to write a computed property:
nameWithDefault: computed('name', {
get() {
return get(this, 'name') || 'tom';
},
set(key, val) {
set(this, 'name', val);
}
})
This is explicit, does not write down the data after a component is viewed, and works for the user.
I agree with the solution from Lux but if you want to try something else, maybe on your child component you could add a computed property that checks the name or default to another string. This allow you to have different default values on the different child templates (assuming you have different child components that use the same name property).
child-component.js
export default Ember.Component.extend({
child_name: Ember.computed('name', function() {
return this.get('name') || 'tom';
}),
});
child-component.hbs
{{child_name}}
<br>
{{surname}}

Emberjs: how to reach the action of a parentView within an itemsViewClass

Cannot get my head around the following problem. I got a Uncaught TypeError: Cannot read property 'send' of undefined whatever I try. I think it must be the controllerBinding in the itemsViewClass, however I think it is defined correctly.
In the code below there are two showMenu actions. The first one works, but the last one in the itemsViewClass does not.
Please take a look at my code below (I show only the relevant code):
//views/menu.js
import Ember from "ember";
var MenuitemsView = Ember.View.extend({
template: Ember.Handlebars.compile('<div{{action "showMenu" target="view"}}>this works already</div>\
much more code here'),
contentBinding: 'content',
itemsView: Ember.CollectionView.extend({
contentBinding: 'parentView.subCategories',
itemViewClass: Ember.View.extend({
controllerBinding: 'view.parentView.controller', // tried to add controllerBinding but did not help
// this is where the question is all about
template: Ember.Handlebars.compile('<div {{action "showMenu" target="parentView"}}>dummy</div>')
}),
actions: {
showMenu: function(){
// dummy for testing
console.log('showmenu itemsView');
}
}
}),
actions: {
showMenu: function() {
console.log('showMenu parentView!'); // how to reach this action?
}
}
});
export default MenuitemsView;
I have tested with {{action "showMenu" target="view"}} and without a target. It seems not to help.
Do someone have a clue why the second showMenu action cannot be reached?
OK, so this is by no means the only way to do logic separation in Ember, but it's the method I use, and seems to be the method generally used in the examples across the web.
The idea is to think of events and actions as separate logic pools, where an event is some manipulation of the DOM itself, and an action is a translatable function that modifies the underlying logic of the application in some way.
Therefore, the flow would look something like this:
Template -> (User Clicks) -> View[click event] -> (sends action to) -> Controller[handleLogic]
The views and the controllers are only loosely connected (which is why you can't directly access views from controllers), so you would need to bind a controller to a view so that you could access it to perform an action.
I have a jsfiddle which gives you an idea of how to use nested views/controllers in this way:
jsfiddle
If you look at the Javascript for that fiddle, it shows that if you use the view/controller separation, you can specifically target controllers to use their actions, utilising the needs keyword within the controller. This is demonstrated in the LevelTwoEntryController in the fiddle.
At an overview level, what should happen if your bindings are correct, is that you perform an action on the template (either by using a click event handler in the view, or using an {{action}} helper in the template itself, which sends the action to the controller for that template. Which controller that is will depend on how your bindings and routing are set up (i've seen it where I've created a view with a template inside a containerView, but the controller is for the containerView itself, not the child view). If the action is not found within that controller, it will then bubble up to the router itself (not the parent controller), and the router is given a chance to handle the action. If you need to hit a controller action at a different level (such as a parent controller or sibling), you use the needs keyword within the controller (see the fiddle).
I hope i've explained this in an understandable way. The view/controller logic separation and loose coupling confused me for a long time in Ember. What this explaination doesn't do, is explain why you are able to use action handlers in your view, as I didn't even know that was possible :(

Observing controller property from view only works if get('controller') called from didInsertElement

Note the below Ember view definition. If I remove the didInsertElement call or comment out the get('controller') call, the setupMultiselect observer never gets called. Is this a feature or a bug? Confused...
Discourse.KbRelatedObjView = Discourse.View.extend({
...
didInsertElement: function() { var self = this;
// for some reason this needs to be here else the observer below never fires
self.get('controller');
},
setupMultiselect: function() { var self = this;
...
}.observes('controller.objPage')
});
I wouldn't say it's a feature or a bug, more like a quirk. It is the expected behavior though. It's noted here.
UNCONSUMED COMPUTED PROPERTIES DO NOT TRIGGER OBSERVERS
If you never get a computed property, its observers will not fire even if its dependent keys change. You can think of the value changing from one unknown value to another.
This doesn't usually affect application code because computed properties are almost always observed at the same time as they are fetched. For example, you get the value of a computed property, put it in DOM (or draw it with D3), and then observe it so you can update the DOM once the property changes.
If you need to observe a computed property but aren't currently retrieving it, just get it in your init method.

Using Ember.js, how do I run some JS after a view is rendered?

How do I run a function after an Ember View is inserted into the DOM?
Here's my use-case: I'd like to use jQuery UI sortable to allow sorting.
You need to override didInsertElement as it's "Called when the element of the view has been inserted into the DOM. Override this function to do any set up that requires an element in the document body."
Inside the didInsertElement callback, you can use this.$() to get a jQuery object for the view's element.
Reference: https://github.com/emberjs/ember.js/blob/master/packages/ember-views/lib/views/view.js
You can also use afterRender method
didInsertElement: function () {
Ember.run.scheduleOnce('afterRender', this, function () {
//Put your code here what you want to do after render of a view
});
}
Ember 2.x: View is deprecated, use component instead
You have to understand the component's lifecycle to know when does certain things happen.
As components are rendered, re-rendered and finally removed, Ember provides lifecycle hooks that allow you to run code at specific times in a component's life.
https://guides.emberjs.com/v2.6.0/components/the-component-lifecycle/
Generally, didInsertElement is a great place to integrate with 3rd-party libraries.
This hook guarantees two (2) things,
The component's element has been both created and inserted into the DOM.
The component's element is accessible via the component's $() method.
In you need JavaScript to run whenever the attributes change
Run your code inside didRender hook.
Once again, please read the lifecycle documentation above for more information
Starting with Ember 3.13, you can use components that inherit from Glimmer, and this example below shows what that could look like:
import Component from '#glimmer/component';
import { action } from '#ember/object';
/* global jQuery */
export default class MyOctaneComponent extends Component {
#action configureSorting(element) {
jQuery(element).sortable();
}
}
<div {{did-insert this.configureSorting}}>
<span>1</span>
<span>2</span>
<span>3</span>
</div>
These view style components don't have lifecycle hooks directly, instead, you can use render-modifiers to attach a function. Unofficial introduction to modifiers can be found here
The benefit of this is that, it's clearer what the responsibilities of the template are and become.
Here is a runnable codesandbox if you want to play around with this:
https://codesandbox.io/s/octane-starter-ftt8s
You need to fire whatever you want in the didInsertElement callback in your View:
MyEmberApp.PostsIndexView = Ember.View.extend({
didInsertElement: function(){
// 'this' refers to the view's template element.
this.$('table.has-datatable').DataTable();
}
});