How to pass an "action" from a template to it's grand child components in Ember Octane - ember.js

I am trying to pass an "action" from the controller to the grandchild component of the current template. But it fails for somereason. Could anyone let me know what am I missing here.
MainTemplate's Router Controller
export default class MainTemplateController extends Controller {
field = "userId";
#action
save () {
//save data
}
}
MainTemplate.hbs
<ChildComponent #field={{this.field}} #save={{this.save}} />
ChildComponent.hbs
<GrandChildComponent #field={{this.field}} #save={{this.save}} />
GrandChildComponent.hbs
<button #onClick={{action "doSomething" (readonly #field)}}>Save</button>
export default class GrandChildComponent extends Component {
#action
doSomething(fieldName) {
console.log(fieldName); // logs as "userId"
console.log(this.args.save) // undefined
}
}

Your code looks fine. There is a small #argument issue in your ChildComponent.hbs file.
Since you are passing the argument from the MainTemplate (save and field) to GrandChildComponent via ChildComponent. The GrandChildComponent invocation should be something like,
<!-- ChildComponent.hbs -->
<GrandChildComponent #field={{#field}} #save={{#save}} />
as these two properties are argument to the ChildComponent component and it's not owning them. Hope this solves your issue and this cheat sheet helped me to understand octane better :)

Related

How to access child component from parent component in Ember

The concept is fairly simple.
Assume I have a child component with its own separate js and hbs.
Child-component.hbs =>
<Button options = {{listOptions}} selected={{selectedOption}}
> Submit <Button
Child-component.js
listOptions =[{id: 124, name: 'Mywork'}
selected => performing an action after getting the value from hbs selection.
Now I'm importing this into another component Main-component.hbs like this
<ChildComponent />
This is rendering as expected with options and all, but based on the selectedOption I want to do something in my main component. Handling the action in the Main-component is not an option for me as its not what I was told to do. Is it possible to access selectedOption from the main-component? Kindly help.
Please note that I want to achieve this in Octane version.
the only way to do this is to specifically pass a reference from the parent to the child, and have the child invoke some function on the parent (because, in rendering, data only flows down by default ("data down, actions/functions up"))
Example:
class Parent {
foo(child) {
// do something with the child
}
}
<Child #foo={{this.foo}} />
// ------------------
class Child { }
{{ (#foo this) }}
Note that this is an "effect", and effects are generally a code-smell (derived data should be used whenever possible)

Getting error after migrating to Ember octane

I am using Ember 3.16.3 and i am getting this error on the following code :
Error: Assertion Failed: You must pass a function as the second
argument to the on modifier
//login.hbs
<form {{on "submit" this.login}}>
<Input type="email" placeholder="email" #value={{this.email}} />
<button type="submit">login</button>
</form>
.
//login.js
import Route from '#ember/routing/route';
import { tracked } from '#glimmer/tracking';
import { action } from '#ember/object';
export default class LoginRoute extends Route {
#tracked email = '';
#action
login(event) {
event.preventDefault();
// do some operations ...
}
}
As specified in the error, the on modifier should receive a valid function to execute. As mentioned in the guides,
If you add the {{action}} helper to any HTML DOM element, when a user clicks the element, the named event will be sent to the template's corresponding component or controller.
This holds good for on modifier or any values that are used in the template. You can think of routes to be a part where we fetch data for the corresponding page. Any other backing property or computation has to be defined inside a controller or component to be used in the template.
Hence, moving your login action to a Controller will solve this issue. Additionally, you need to move email to the controller as well, or you won't see updates to it work correctly.

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.

transitionToRoute('route') From Inside Component

How do I transition to a route pragmatically from inside a component action?
I tried to use #get('controller').transitionToRoute('images'), but the controller refers to the component itself. I understand that components should be self contained, so should I be using a view instead to interact with controllers/routes better?
Example
App.ImageEditorComponent = Ember.Component.extend
...
actions:
delete: ->
App.Files.removeObject(object)
#transitionToRoute('images') # This throws an exception
...
You could pass the controller in via a binding and then access it inside your component like so:
{{image-editor currentControllerBinding="controller"}}
App.ImageEditorComponent = Ember.Component.extend
...
actions:
delete: ->
App.Files.removeObject(object)
#get('currentController').transitionToRoute('images')
...
Create action on parent controller.
export default Ember.Controller.extend({
actions: {
transInController() {
this.transitionToRoute('home')
}
}
});
Specify this action on component call.
{{some-component transInComponent=(action "transInController")}}
AFTER v3.4.0 (August 27, 2018)
some-component.js
export default Component.extend({
actions: {
componentAction1() {
this.transInComponent();
}
}
});
OR simpler in some-component.hbs
<button onclick={{#transInComponent}}>GO HOME</button>
BEFORE v3.4.0
Ember.component.sendAction
"Send Action" from component up to controller
export default Ember.Component.extend({
actions: {
componentAction1() {
this.sendAction('transInComponent');
}
}
});
A component is supposed to be isolated from its context, so while you could pass in a reference to the controller, that's probably outside the scope of what a component is for. You might want to just stick with using a view with its own controller instead. Check out Views Over Components - An Ember Refactoring Story.
From Ember.js, Sending Actions from Components to Your Application, there's discussion about sending actions from a component up the route hierarchy.
A lot of things changed in Ember since the original post. So maybe today the best option would be to pass down to the component a route action that takes care of the transition (maybe using the fancy addon ember-cli-route-action.
Otherwise you can create an initializer with ember g initializer router and inside put in there a code like this one
export function initialize (application) {
application.inject('route', 'router', 'router:main')
application.inject('component', 'router', 'router:main')
}
export default {
name: 'router',
initialize
}
This way you can access the router in your component with this.get('router') and, for instance, perform a transition
this.get('router').transitionTo('images')
At component.HBS component file make a
{{#link-to "routeDestinationYouWant" }}
For Example:
<section class="component">
{{#link-to "menu.shops.category.content" "param"}}
<figure id="{{Id}}" class="list-block yellow-bg text-center" {{action "showList" "parameter"}}>
<section class="list-block-icon white-text my-icons {{WebFont}}"></section>
<figcaption class="list-block-title black-text">{{{Title}}}</figcaption>
</figure>
{{/link-to}}
</section>
I've written this answer for another similar question.
If you want to use the router only in a specific component or service or controller, you may try this:
Initialize an attribute with the private service -routing. The - because it's not a public API yet.
router: service('-routing'),
And then inside any action method or other function inside the service or component:
this.get('router').transitionTo(routeName, optionalParams);
Note: It'll be transitionToRoute in a controller.
Link to question: Ember transitionToRoute cleanly in a component without sendAction
Link to answer: https://stackoverflow.com/a/41972854/2968465

How to get reference of View in Controller

I am using Ember 1.0pre and following Ember suggested application structure (Using Router).
For form validation, I want to call $('form').valid() method on Button click.
So I have following method in my view
validate: function(){
return this.$('form').valid()
}
Action in template file:
<button type="submit" class="btn" {{action doSaveSettings this}}>Save Changes</button>
and doSaveSettings method is in Controller.
How can I get instance of view in controller, for calling validate method?
EDIT:
In controller, this.view is null. I have put {{debugger}} in template and this refers to
<App.XyzController:ember1062> and this.view is null.
The default target of actions have been changed from the view to the router in ember 0.9.8.1 (I believe). To set the target to the view you need to override it like this
<button type="submit" class="btn" {{action doSaveSettings target="view"}}>Save Changes</button>
edit: Your controllers should not know about the view.
In ember the purpose of a view is only i repeat is only to handle events or to create reusable components
i would not suggest this since there is always a reason why views are not meant to be accessed from controller and its good to follow it, however if you really do want to use it you could do the below two ways:
I dont know if u r using ember-cli or ember but the logic is same. The answer however is for ember-cli
//Inside appname/controller/your-conroller.js
import reqdView from 'appname/views/your-view';
//Lets assume u want to call a function called validate inside view
//Add this statement inside the controller to run the validate function
reqdView.prototype.validate();
OR
var reqdViewInst = new reqdView();
reqdViewInst.validate();
If you want to validate the view then do the validation inside didInsertElement
export default Ember.View.extend({
didInsertElement:function()
{
this.validate();
},
validate:function()
{
//do your validation
}
});
OR
export default Ember.View.extend({
eventManager: Ember.Object.create({
didInsertElement:function(event, view)
{
view.validate();
}
}),
validate:function()
{
//do your validation
}
});