Trigger route action from component - javascript

I have a component that I want to trigger a route level action on so I can transition to a different route.
App = Ember.Application.create();
App.Router.map(function() {
});
App.IndexRoute = Ember.Route.extend({
actions: {
complete: function() {
// This never happens :(
console.log('Triggered complete!');
}
}
});
App.MyAreaComponent = Ember.Component.extend({
actions: {
clickMyButton: function() {
console.log('Triggering complete action.');
// Attempting to trigger App.IndexRoute.actions.complete here
this.sendAction('complete');
}
}
});
What I am trying to accomplish is when MyAreaComponent's 'clickMyButton' action is triggered, it will trigger the IndexRoute's 'complete' action.
I have set up a jsbin to demonstrate my issue:
http://emberjs.jsbin.com/wivuyike/1/edit
According to the EmberJs documentation on action bubbling:
If the controller does not implement a method with the same name as the action in its actions object, the action will be sent to the router, where the currently active leaf route will be given a chance to handle the action.
So with this in mind I would expect the component to say "Let's check my controller and see if it has an action called 'complete'. No? Ok, let's check my route (IndexRoute) to see if it has an action called 'complete'. Yes? Ok, trigger it!"
The only thing I can think of is that because of the way the component is set up, IndexRoute isn't set as the route of the component, so the action bubbling just stops at the controller.
I'm unsure where to go from here. Do I need to do something special to make my component aware of my IndexRoute?

Here is your updated sample -
http://emberjs.jsbin.com/wivuyike/3/edit
So from the component your action need to bubble herself up, this can be done by
this.sendAction('clickMyButton');
and then when you use your component, assign route action which needs to be triggered to your component action as below
{{my-area clickMyButton='complete'}}

in the template next to the route:
{{component-name refresh=(route-action "refresh")}}
then in component you can call it:
this.get('refresh')();
An action can be called from a component template by creating another action in the component.
So it saves me time today.

If you are doing nothing else in the action, except for sending an action to the route, you can try doing this directly from the template:
<button {{action "complete" target="route"}}>Complete</button>
(Untested code, make sure you test first!)
... otherwise it looks like you should do:
this.sendAction('action', 'complete');
instead of:
this.sendAction('complete');
So it looks like you were just missing that first parameter in the code posted in your question. Reference

Related

How to open and close Ember Power Select from outside

I'm aware of this question, but is not complete. I want to open and close the dropdown from outside.
I can dispatch a mousedown event when click on my wrapper component, so ember-power-select trigger opens!. But then if I click again it doesn't close. More precisely, it closes and opens again rapidly.
My assumption is the component is listening blur event to get closed, and then the mousedown arrives again and open the trigger.
Has anyone managed to get this to work? or an alternative?? I'm quite lost :)
Thanks for the help!
wrapper-component.js
didInsertElement() {
this._super(...arguments);
this.element.addEventListener('mousedown', (event) => {
event.stopPropagation();
const eventedElement = this.element.querySelector('.ember-power-select-trigger');
const mouseDownEvent = new MouseEvent('mousedown');
eventedElement.dispatchEvent(mouseDownEvent);
});
},
According to the docs, the only way to interact with the trigger/component is through the read-only API provided in subcomponents, blocks, and actions of the ember-power-select component.
Since you can already open the trigger you can cache the API in an onchange event action defined in your component (or controller of the route) where you render the ember-power-select:
Where you render the component just provide an action to onopen:
{{#ember-power-select
options=options
selected=selectedOption
onchange=(action "someAction")
onopen=(action "cacheAPI")
as |option|}}
{{option}}
{{/ember-power-select}}
In your component or controller that renders it:
actions: {
cacheAPI(options) {
if (this.powerSelectAPI) return;
this.set('powerSelectAPI', options);
// if you just want the actions:
// this.set('powerSelectAPI', options.actions);
}
}
Then you can open/close the trigger through the API:
this.get('powerSelectAPI').actions.close();
RegisterAPI is solution.
From the docs:
The function that will be called when the component is instantiated and also when any state changes inside the component, and receives a publicAPI object that the user can use to control the select from the outside using the actions in it.
#action
registerAPI(emberPowerSelect) {
if (!this.emberPowerSelect) {
this.emberPowerSelect = emberPowerSelect;
}
}
#action
toggle(state) {
if (state) {
this.emberPowerSelect.actions.open();
} else {
this.emberPowerSelect.actions.close();
}
}

How to listen to redux action stream in component

I'm working on a React application that uses the following architecture:
redux
typesafe-actions
redux-observable
My question is: How can I execute an UI action on specific redux action?
For example, suppose we have the following async actions defined with typesafe-actions:
export const listTodo = createAsyncAction(
'TODO:LIST:REQUEST',
'TODO:LIST:SUCCESS',
'TODO:LIST:FAILURE',
)<void, Todo[], Error>();
An Epic will watch for listTodo.request() and send the API call, then convert the response to a listTodo.success() action. Then the redux reducer will be triggered by listTodo.success() action and store the todo list into redux store.
In this setting, suppose I want to do the following things in an component:
dispatch a listTodo.request() action to retrieve all the actions
After the async request is done (i.e. after listTodo.success() action appears in the action stream), redirect the UI to a second path
So my question is, how could I watch the action stream and react to the listTodo.success() action?
UPDATE: To avoid being too specific, we can think of another case. I want to simply display an alert with window.alert() after listTodo.success() appears in the action stream. Or simply console.log(), or whatever that changes local state (instead of global redux state). Is there a way to implement that?
UPDATE 2: There is a similar question here, but for Angular w/ ngrx. What I want to do is exactly the thing described in above post, but in React / redux-observable fashion:
import { Actions } from '#ngrx/effects';
#Component(...)
class SomeComponent implements OnDestroy {
constructor(updates$: Actions) {
updates$
.ofType(PostActions.SAVE_POST_SUCCESS)
.takeUntil(this.destroyed$)
.do(() => /* hooray, success, show notification alert ect..
.subscribe();
}
}
With redux the components update based on state.
If you want to update a component based on an action than you update the state in the reducer, such as setting {...state, success: true} in the reducer. From there you simply read the state into your component as you normally would and if the state is changing to success than you show your window.
Might be a little late but I solved a similar problem by creating a little npm module. It allows you to subscribe to and listen for redux actions and executes the provided callback function as soon as the state change is complete. Usage is as follows. In your componentWillMount or componentDidMount hook:
subscribeToWatcher(this,[
{
action:"SOME_ACTION",
callback:()=>{
console.log("Callback Working");
},
onStateChange:true
},
]);
Detailed documentation can be found at https://www.npmjs.com/package/redux-action-watcher
I feel like a dialogue should be a side effect, so you'd put them in epics

Ember Component does not reload on route reload or transitionTo

I have an ember route that calls an ember component to display all the data.
I have a case in which the route's key changes and I reload using route.transitionTo. The data is loaded fine. But the problem is the component does stuff in the init and didInsertElement methods, which on the route.transitionTo aren't triggered.
How can I make sure on transitionTo the component is called again?
I can't use route.refresh because that doesn't do the model hook.
Any help will be appreciated, Thank you!
Well, there is a harder and better way, and there is a lazy and fast way:
Better way: I suggest you to create a property in the route and pass this parameter to the component, and in the component create a computed property for that parameter, what you've sent, and there you can run your code what you called in "init" and "didInsertElement" function.
E.g.:
router.hbs
{{component-name parameter=parameter}}
Component.js
lineWidth: Ember.computed('parameter', function() {
// I will run here if I get the parameter (and the parameter changed in route)
}),
Not the best way:
You can reload the whole component like this:
router.hbs
{{#if reload}}
{{component-name}}
{{/if}}
router.js
this.set('reload', false);
Ember.run.next(() => {
this.set('reload', true);
});

Ember component test -- how unit test component's action when it doesn't invoke an external action?

I'm somewhat new to Ember and trying to test a pager component. Simplified component looks like this:
export default Ember.Component.extend({
index: 0,
actions: {
next() {
this.incrementProperty('index');
}
}
});
I'm trying to test that the next() action does indeed increment the index property as expected. I wrote a unit test like this:
test('next should increment the index by 1', function (assert) {
const component = this.subject();
assert.equal(component.get('index'), 0, 'Index should be 0');
component.get('actions').next();
assert.equal(component.get('index'), 1, 'index should be 1');
});
But it fails with the error "this.incrementProperty is not a function". Debugging, the "this" in the test, when inside the next() function, isn't the context of the component -- it is an object with next() as its only property.
I'm wondering if this is because of how I'm invoking the nextPlace action in the test. I know I could write an integration test that clicks the button that fires this action and compares some UI to make sure it changed, but that seems very convoluted when all I'm trying to test is that the function itself performs as expected. If it were an action passed to this component (closure action) I know I could set up a dummy function in the integration test, pass that to the component, and see how it responds. But this action doesn't call an action passed down to it.
I realize the function I'm testing is really basic, but partly this is to understand how to test actions in components that don't call an external (to the component) action.
If you didn't express your motivation about not writing an integration test, I would only suggest you to write an integration test. IMO, your reason is valid. I've 3 suggestions for booting the unit test for your case.
First of all: The reason of not working is "component.get('actions').next get a reference to the next function without any context. So this is not a valid in that call. To make it valid, just bind component to it as shown below:
component.get('actions').next.bind(component)();
But I wouldn't prefer this because it is extracting the next from its context and bind it again. We are doing this bind thing, because we know that the next function has a reference to this in its code.
So my second suggestion is "triggering the event some how". For triggering it, have a look at the following code:
component.send('next');
IMO, this is better. We don't need to know "what next is doing". We are just triggering an event.
But this is dealing with ember component's lifecycle. We accept this situation: There is a specific hash called actions and we can trigger it via send. (This is completely okay.) Instead of dealing with actions, we can seperate doing something from action handling. So you can define another function to do what you need, and simply call it from your action handler. (Ok, in this case we again need to know what function is called by action handler. But this seems more clear for me.) As shown below:
next(){
this.incrementProperty('index');
},
actions:{
next(){
this.next();
}
}
You can see all three alternatives at this twiddle.

Flux / Fluxible: Updating routes based on state change

What is the most concise way to trigger route changes based on a change to a state store, using Fluxible and react router?
An example component might take some user input and call an Action on a click event (shortened for brevity)
class NameInput extends React.Component {
constructor (props) {
super(props);
this.state = props.state;
this.handleClick = this.handleClick.bind(this);
}
handleClick (event) {
this.context.executeAction(setName, {name:'Some User Value'});
}
render () {
return (
<div>
<input type="button" value="Set Name" onClick={this.handleClick} />
</div>
);
}
}
export default Home;
The handleClick method executes an Action which can update a Store with our new value.
But what if I also want this to trigger a navigation after the Store is updated? I could add the router context type and directly call the transition method after executing the Action:
this.context.executeAction(setName, {name:'Some User Value'});
this.context.router.transitionTo('some-route');
But this assumes that the setName Action is synchronous. Is this conceptually safe, on the assumption that the new route will re-render once the Action is completed and the Store is updated?
Alternatively, should the original Component listen for Store Changes and start the route transition based on some assessment of the store state?
Using the Fluxible, connectToStores implementation, I can listen for discreet changes to Store state:
NameInput = connectToStores(NameInput, [SomeStore], function (stores, props) {
return {
name: stores.SomeStore.getState().name
}
});
How a could a Store listener of this type be used to initiate a Route change?
I've noticed in my own application that for these kinds of flows it's usually safer to let actions do all the hard work. Annoying thing here is that the router isn't accessible from actions.
So, this is what I'd do:
Create a meta-action that does both: setNameAndNavigate. As payload you use something like this:
{
name: 'Value',
destination: {to: 'some-route', params: []},
router: this.context.router
}
Then, in your action, do the navigating when the setName completes.
It's not ideal, especially the passing of the Router to the action. There's probably some way to attach the Router to the action context, but that's not as simple as I had hoped. This is probably a good starting point though.
Extra reading:
Why do everything in actions? It's risky to execute actions in components in response to store changes. Since Fluxible 0.4 you can no longer let actions dispatch inside another dispatch. This happens a lot faster than you think, for example, executing an action in response to a change in a store, without delaying it with setImmediate or setTimeout, will kill your application since store changes happen synchronously during a dispatch.
Inside actions however, you can easily execute actions and dispatches, and wait for them to complete before executing the next one.
The end result of working this way is that most of your application logic has moved to actions, and your components turn into simple views that set-and-forget actions only in response to user interactions (clicks/scrolling/hover/..., as long as it's not in response to a store change).
The Best way is to create a new action as #ambroos suggested,
setNameAndNavigate. For navigation though, use the navigateAction
https://github.com/yahoo/fluxible/blob/master/packages/fluxible-router/lib/navigateAction.js, you would only have to give the url as argument.
Something like this,
import async from 'async';
import setName from 'some/path/setName';
export default function setNameAndNavigate(context, params, done) {
async.waterfall([
callback => {
setName(context, params, callback);
},
(callback) => {
navigate(context, {
url: '/someNewUrl',
}, callback);
}
], done);
}
let your actions be the main workers as much as possible.

Categories