Ember: globally-available search component (and action)? - ember.js

I want to have a search field inside a component that can be placed anywhere in the app. It can appear on any template, or nested in components. The search form would accept user input (search term) and submit would trigger a search action which transitions to a results template.
Seems simple enough, but I can't figure out how to make an action globally available. And if I could, how do you pass the inputted term to the action in the first place? There's surprisingly little info on how to handle form submits with Ember CLI.
Thus far I've just been submitting a regular form with action='/results'. But that's obviously reloading the app.
I've been messing with creating an action in the index controller like this:
export default Ember.Controller.extend(defaultParams, {
term: '',
actions: {
keywordSearch() {
this.transitionToRoute('results', { queryParams: { q: this.get('term') }});
}
}
});
Then passing a closure action down to my search component, which is nested 2 deep from the index template.
index.hbs:
{{index-search keywordSearch=(action "keywordSearch")}}
index-search.hbs (component):
{{search-field keywordSearch=keywordSearch }}
search-field.hbs (nested component):
<form {{ action (action keywordSearch) on='submit' }}>
{{ input value=term }}
<button type="submit">Search</button>
</form>
And that will run the action, but the term is not supplied. How do you supply term to the closure action?
And...do I really need to pass the action down to every single place the search field is going to appear in the app, or is there an easier way to do it?

Instead of writing actions in all components and routes, you can create a service for search. Inject the service into the component and handle the route transition from service method. Check the sample code below,
Search-component.hbs
<form {{ action (action search) on='submit' }}>
{{ input value=keyword }}
<button type="submit">Search</button>
</form>
Search-component.js
export default Ember.Component.extend({
globalSearch: Ember.inject.service('search'),
actions: {
search() {
const { keyword } = this.getProperties('keyword');
this.get('globalSearch').showResults(keyword).then(() => {
alert('Success');
}, (err) => {
alert('Error while searching: ' + err.responseText);
});
}
}
});
Service - app/services/search.js
import Ember from 'ember';
export default Ember.Service.extend({
init() {
this._super(...arguments);
},
showResults(keyword) {
// write code for transition to search results route here
}
});

Related

Ember component call an action in a route or controller

I have a component the main purpose of which is to display a row of items.
Every row has a delete button to make it possible to delete a row. How is possible to pass an action from a template to the component which will trigger an action in a router ?
Here is the template using the component:
#templates/holiday-hours.hbs
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true}}
{{/each}}
Here is the component template:
# templates/components/holiday-hour.hbs
...
div class="col-sm-1">
{{#if shouldDisplayDeleteIcon}}
<button type="button" class="btn btn-danger btn-sm mt-1" {{action 'deleteHoliday' holiday}}>
<span class="oi oi-trash"></span>
</button>
{{/if}}
</div>
I'm using the same component to display a row and to create a new item (holiday-hour).
I'm using ember 3.1.2
Thank you
You have to send the actions up from the component to the route. The main way to do this is by adding actions to your component that "send" the action to the parent. Once the action is sent you have to tell the component what action on the route to trigger by passing in the action as a parameter. Below is an example of how to do this.
Component js
# components/holiday-hour.js
...
actions: {
deleteHoliday(){
this.sendAction('deleteHoliday');
}
}
Template for route
#templates/holiday-hours.hbs
...
{{#each model as |holidayHour|}}
{{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true deleteHoliday='deleteHoliday'}}
{{/each}}
Route js
#routes/holiday-hours.js
...
actions: {
deleteHoliday(){
//code to delete holiday
}
}
I will try to give a general answer because your question is not giving enough/all info regarding the route actions etc. Long answer short, using closure functions. Assuming this is your route js file routes/holiday-hours.js
import Route from '#ember/routing/route';
export default Route.extend({
model(){ /*... some code */ },
setupController(controller){
this._super(controller);
controller.set('actions', {
passToComponent: function(param) { //.... function logic }
})
}
});
Note: in the above snippet, I'm using setupController to create actions. Alternatively, you can put the actions inside a controller file otherwise actions directly inside the route will throw an error.
So I want the action passToComponent to be called from the component. This is what you do to make it accessible inside the component.
{{#each model as |holidayHour|}} {{holiday-hour holiday=holidayHour shouldDisplayDeleteIcon=true callAction=(action 'passToComponent')} {{/each}}
Now we have passed the action to the component and here's how to call it from the component. Note: I have added a param just to show that it can take a param when called within the component.
import Component from '#ember/component';
export default Component.extend({
actions: {
deleteHoliday: ()=> {
this.get('callAction')() /*Pass in any params in the brackets*/
}
}
});
You will also see demonstrations using sendAction which is rather old and acts more of an event bus that is not very efficient. Read more from this article

Ember : Close/Cancel a form using 'Esacpe' Key

I have form and and I want to close my form with the escape key. I can't figure out how to implement it. I know I need some keyDown event or so! but where to implement and how do I make it trigger?
<form id = 'myform' {{action 'save' on='submit'}}>
<div class="usersgroups-headline-label admin-form-headline-label">
{{#if page.isEdit}}Edit{{else}}Add{{/if}}{{#if page.holder.isUser}} User{{#if page.isEdit}} : {{page.oldHolder.Id}}{{#if showHolderName}} ({{page.oldHolder.Name}}){{/if}}{{/if}}{{/if}}{{#if page.holder.isGroup}} Group{{#if page.isEdit}} : {{page.oldHolder.Id}}{{/if}}{{/if}}
</div>
.
.
.
.
<div class="action-buttons">
{{#if page.isEdit}}
{{form-button value='Save' enabled=page.canUpdate type='submit'}}
{{else}}
{{form-button value='Add' enabled=page.canSave type='submit'}}
{{/if}}
{{form-button value='Cancel' action='cancel'}}
</div>
I suggest using an addon such as ember-keyboard which abstracts the behavior you describe. I can also recommend to take a look at ember-modal-dialog which, while not what you may look for at first glance provides similar behavior and also explains a lot on how to implement keyboard events in their own README.
If you want to handle this by hand, you should wrap your form in a component that handles the keyboard event for you, if the form is not in a component already.
Inside the form you will have to use the didInsertElement method to start listening for the event and the willDestroyElement method to stop listening when the component gets teared down.
In current Ember (~2.18) this looks something like this:
import Component from '#ember/component';
export default Component.create({
onEscapeKey: () => {},
didInsertElement() {
this._escapeKeyCallback = (event) => {
if (event.key === 'Escape') {
this.onEscapeKey();
}
};
window.addEventListener('keypress', this._escapeKeyCallback);
},
willDestroyElement() {
window.removeEventListener('keypress', this._escapeKeyCallback);
},
});
You can then use this component inside your template and use the onEscapeKey property to trigger an action when the escape key is pressed.
{{escape-key-component onEscapeKey=(action 'doSomething')}}

Ember JS - Add select box option to URL string via Query Param

I have a Select Box in my form and I would like to be able to use the selection as a Query Param so that I can refresh a model based on its selection. The Select Box is from this ember add-on.
{{#select-box/native value=sb name=module on-select=(action 'selected') class="form-control" as |sb| }}
{{sb.option value='Select a Module:'}} {{sb.option value='Option1'}} {{sb.option value="Option2" }} {{sb.option value="Option3" }} {{sb.option value="option4" }}
{{/select-box/native}}
The 'selected' action simply adds the option to a variable so that I can use it later in a switch statement:
selected(x) {
module = x
},
I'd like to have the selection (or a representation of the selection) in my URL string but I can'tt work out how. I have other inputs building into the URL string but none of them are select boxes.
I have a 'module' item in the QueryParams on my route but it doesn't do anything, I suspect I'll have to do something in the 'selected' action but I'm not sure what.
I haven't used the add-on you mentioned, but here is how you can do it using normal <select>, so just bridge the gap between normal <select> and the add-on you are using in terms of making sure that the status variable in the example below changes depending on what you select in your select box - Ember will do the rest.
Here's a configuration that works if you want to filter a list of users based on the status value you select from a dropdown:
// app/models/user.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
status: DS.attr('string')
});
// app/templates/users.hbs
<select onchange={{action (mut status) value="target.value"}}>
<option value="" selected={{eq status ''}}>- All users -</option>
<option value="active" selected={{eq status 'active'}}>Active</option>
<option value="inactive" selected={{eq status 'inactive'}}>Inactive</option>
</select>
<ul>
{{#each model as |user|}}
<li>{{user.name}}, {{user.status}}</li>
{{/each}}
</ul>
// app/controllers/users.js
import Controller from '#ember/controller';
export default Controller.extend({
queryParams: ['status'],
status: ''
});
// app/routes/users.js
import Route from '#ember/routing/route';
export default Route.extend({
queryParams: {
status: {
refreshModel: true
}
},
model(params) {
var options = {};
if (params.status) {
options.status = params.status;
}
return this.get('store').query('user', options);
}
});
How does it work?
In the controller you define a property status, which you also indicate to be a query parameter (in the URL). Then in the route, you also define status to be a query parameter which refreshes the model. In the model() hook you extract the parameter and use it for Ember Data Store's query() to fetch the model every time you change the value of status. Your route URL will have ?status=... appended to it, and your server will receive a request similar to example.com/api/users?status=.... Of course, you can configure options in the model() hook differently to format the request URL for the server, but I kept it like this for the sake of simplicity.
The only thing that might be confusing is the template file. Apart from the {{eq status '...'}} syntax, which is a truth helper that simply determines whether the option is selected, the rest of the selecting simply aims to change the status variable (explained in depth here).

Ember: How do I get simple form data without a model?

I'm trying to do get a single string from a form input and log it to the console. Is there a way to do this without using an Ember Data model?
Here's the form component in Handlebars:
// my-form.hbs
<form {{action "grabNewEmail" on="submit"}}>
{{input value="Your Email Address"}}
<button class="btn btn-primary">Get Early Access</button>
</form>
And here's the current code from the component:
// my-form.js
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
grabNewEmail() {
let email = /* I want to get this string from the input on submit */
console.log(email);
}
}
});
Is it possible to simply grab the input string, or am I required to create a model?
Thanks!
Instead of using a model, you can use a component property to store and retrieve the data.
In your hbs:
{{input value=newEmailAddress}}
And in your component you can do:
export default Ember.component.extend({
newEmailAddress: "", // Not really needed, just to be explicit that this variable exists in the component.
actions: {
grabNewEmail () {
let email = this.get('newEmailAddress');
}
}
});

How to communicate nested component to mixin without using service

I have a nested component. The child-component has an input field bind with mixin variable and action, In parent-component had button action. ( without mixin because parent-component considered as a addon ) while button action triggers the child-component value update to the mixin variable. How trigger a child action from parent-component.
Note: Please refer the attached demo link
https://ember-twiddle.com/d8b01ba563b555fc01374f300db20c5b?openFiles=components.child-component.js%2C
An easier approach than triggering actions on your child-component would be to pass the updated_val down to the child component, and let the ember value-binding do the rest. When the value has changed and you click update, make your ajax call from your dialog-component.
e.g. for passing your updated_val down
//application.hbs
{{dialog-component updated_val=updated_val}}
But since your question was "How to trigger actions on a child component", this might be an approach (see updated twiddle):
//dialog-component.js
import Ember from 'ember';
export default Ember.Component.extend( {
actionCalled: false,
actions:{
callChildAction() {
this.toggleProperty( 'actionCalled' );
},
updateValue(updateVal) {
this.set('updated_val', updateVal);
}
}
});
//dialog-component.hbs
<div class='dialog'>
{{!pass your 'updateValue' action from the dialog-component to the child-component}}
{{ child-component actionCalled=actionCalled updateValue=(action 'updateValue')}}
<button {{action 'callChildAction' }}> Update </button>
</div>
//child-component.js
import Ember from 'ember';
export default Ember.Component.extend( {
child_val: '',
actionObserver: Ember.observer('actionCalled', function(){
this.send('childAction')
}),
actions:{
childAction(){
alert( 'childAction called..' );
// some validation and ajax call.
this.sendAction('updateValue', this.get('child_val'));
},
}
});
//child-component.hbs
{{input value=child_val}}