Use closure actions, unless you need bubbling - ember.js

I don't know what's wrong in my code.
template/components/item.hbs:
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default" {{action 'buttonClicked' item}} disabled={{unless item.isValid true}}>{{buttonLabel}}</button>
</div>
</div>
components/item.js:
import Component from '#ember/component';
export default Component.extend({
buttonLabel: 'Save',
actions: {
buttonClicked(param) {
this.sendAction('action', param);
}
}
});
Ember/library-app/app/components/item.js
8:13 error Use closure actions, unless you need bubbling ember/closure-actions

Since ember > 2.0 closure actions are the favored way to handle actions (Data Down Actions Up DDAU).
I would recommend reading this http://miguelcamba.com/blog/2016/01/24/ember-closure-actions-in-depth/
Since newer ember versions(2.18 I believe), there is a ESlint rule to point out that people should move to closure actions: https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/closure-actions.md
You could rewrite your code to:
my-button.hbs
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default" onclick={{action "buttonClicked" item}} disabled={{unless item.isValid true}}>{{buttonLabel}}</button>
</div>
</div>
my-button.js
import Component from '#ember/component';
export default Component.extend({
buttonLabel: 'Save',
actions: {
buttonClicked(param) {
this.get('onButtonClicked')(param);
}
}
});
Or you could wave your action through:
my-button.hbs
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default" onclick={{action onButtonClicked item}} disabled={{unless item.isValid true}}>{{buttonLabel}}</button>
</div>
</div>
my-button.js
import Component from '#ember/component';
export default Component.extend({
buttonLabel: 'Save'
});

actions: {
buttonClicked(param) {
this.sendAction('action', param);
}
}
Instead of the name 'action' try using some other actionName
Like
actions: {
buttonClicked(param) {
this.sendAction('onButtonClick', param);
}
}
Then use it in the parent template as
{{item onButtonClick="someActionHandledInTheParent"}}

Related

Fire form submit event from custom component

I'm using the latest Ember (3.2).
I have made extension of text-area component:
app/components/enterable-textarea.js
export default TextArea.extend({
keyPress(event) {
if (event.keyCode === 13) {
console.info('e ', event);
}
}
});
I see the debug output in the console once I hit the 'Enter' key.
In my route template I have simple form like:
<form {{action "save" model.newNote on='submit'}}>
<div class="form-group">
<label for="tag">Tag</label>
{{input type="text" value=model.newNote.tag
placeholder="#anytag" class="form-control"}}
</div>
<div class="form-group">
<label for="note">Notepad</label>
{{enterable-textarea value=model.newNote.note
rows="6" class="form-control"}}
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
Ho do I pass form action to the component or fire the 'submit' event of the form?
I need to pass whole form to the route action by pressing the 'Enter'
You pass an action as a property and then call it as a function:
export default TextArea.extend({
onEnter: () => {}, //or function() {},
keyPress(event) {
if (event.keyCode === 13) {
this.get('onEnter')(); //or even this.onEnter();
}
}
});
{{enterable-textarea value=model.newNote.note
rows="6" class="form-control" onEnter=(action "save" model.newNote)}}
Read more about actions

Loading data that isnt on the route?

I have a dropdown setup on a form but how do i set it up if my data isnt in the route? I am trying to load in a list of territories on a new dealer form. I have added ember-power-select add on but I cant figure out how to load the data to the add on to display the available options. Do I need to create a service or controller?
New to ember just trying to work through the basics.
Router.map(function() {
this.route('login');
this.route('admin', function() {
this.route('territory', function() {
this.route('new');
this.route('edit', { path: '/:territory_id/edit'});
this.route('view', { path: '/:territory_id/view'});
});
this.route('dealer', function() {
this.route('view', { path: '/:dealer_id/view'});
this.route('new');
});
});
});
This is my form I am using on creating a new item.
<div class="well">
<form class="form-horizontal">
<fieldset>
<legend>New Dealer</legend>
<div class="form-group">
<label for="inputFirstName" class="col-lg-2 control-label">Name</label>
<div class="col-lg-4">
{{input type="text" value=model.dealerName class="form-control" placeholder="Dealer Name"}}
</div>
</div>
<div class="form-group">
<label for="inputFirstName" class="col-lg-2 control-label">Territory</label>
<div class="col-lg-4">
{{#power-select selected=dealer options=dealer onchange=(action "chooseDestination") as |dealer|}}
{{dealer.dealerName}}
{{/power-select}}
</div>
</div>
<div class="form-group">
<div class="col-lg-10 col-lg-offset-2">
{{#link-to 'admin.dealer' class="btn btn-default"}}Cancel{{/link-to}}
<button type="submit" class="btn btn-primary" {{action 'saveDealer' model}}>Submit</button>
</div>
</div>
</fieldset>
</form>
</div>
Just because a route is the new action for a territory or the view action for a dealer, doesn't mean that you can't load other data there.
For example, in the territory/view route, you could load all the dealers.
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return Ember.RSVP.hash({
territory: this.store.find('territory', params.territory_id),
dealers: this.store.findAll('dealer')
})
}
});
Then in your controller and templates, you would have access to model.territory and model.dealers

Ember: Integrating Google Recaptcha with ember-cp-validations

I have a simple contact form, with validation done using ember-cp-validations https://github.com/offirgolan/ember-cp-validations and I now need to integrate the new Google Recaptcha into that.
For the rendering of the recaptcha, I am using this code - https://gist.github.com/cravindra/5beeb0098dda657433ed - which works perfectly.
However, I don't know how to deal with the verification process to allow the form to be submitted/prevented if the challenge is correct/incorrect or not provided
Here is a truncated version of my contact-form component
import Ember from 'ember';
import Validations from './cp-validations/contact-form';
import config from '../config/environment';
export default Ember.Component.extend(Validations,{
data:{},
nameMessage:null,
init() {
this._super(...arguments);
this.set('data',{});
},
actions:{
submitForm() {
this.validate().then(({model,validations}) => {
if (validations.get('isValid')) {
// submit form
}
else {
if(model.get('validations.attrs.data.name.isInvalid')){
this.set('nameMessage',model.get('validations.attrs.data.name.messages'));
}
}
})
}
}
});
Here is the template for the component, which includes the rendering of the recpatcha using the gist above
<form {{action 'submitForm' on='submit'}}>
<div class="row">
<div class="medium-6 columns">
{{input type="text" value=data.name id="name" placeholder="Enter your name"}}
<div class="error-message">
{{nameMessage}}
</div>
</div>
</div>
<div class="row">
<div class="medium-12 columns">
{{google-recaptcha}}
</div>
</div>
<button class="button primary" type="submit">Submit</button>
</form>
The Validations import looks like this
import { validator, buildValidations } from 'ember-cp-validations';
export default buildValidations({
'data.name': {
validators: [
validator('presence',{
presence:true,
message:'Please enter your name'
})
]
},
});
Many thanks for any help!
Register captchaComplete in your google-recaptcha component and mix the answer with your validations
UPDATE
contact-form.hbs
<form {{action 'submitForm' on='submit'}}>
<div class="row">
<div class="medium-6 columns">
{{input type="text" value=data.name id="name" placeholder="Enter your name"}}
<div class="error-message">
{{nameMessage}}
</div>
</div>
</div>
<div class="row">
<div class="medium-12 columns">
{{google-recaptcha captchaComplete=validateRecatcha}}
</div>
</div>
<button class="button primary" type="submit">Submit</button>
</form>
contact-form.js
import Ember from 'ember';
import Validations from './cp-validations/contact-form';
import config from '../config/environment';
export default Ember.Component.extend(Validations,{
data:{},
nameMessage:null,
captchaValidated: false,
init() {
this._super(...arguments);
this.set('data',{});
},
actions:{
validateRecatcha(data){
//if data denotes captcha is verified set captchaValidated to true else false
},
submitForm() {
this.validate().then(({model,validations}) => {
if (validations.get('isValid') && this.get('captchaValidated')) {
// submit form
}
else {
if(model.get('validations.attrs.data.name.isInvalid')){
this.set('nameMessage',model.get('validations.attrs.data.name.messages'));
}
}
})
}
}
});

How to share settings across an application?

I have a main page listing some categories / subcategories. Whenever a subcategory is clicked, the action openSubcategory is triggered:
// routes/application.js
import Ember from 'ember';
export default Ember.Route.extend({
userSelections: Ember.inject.service('user-selections'),
actions: {
openSubcategory: function(categoryId, subcategoryId) {
var userSelections = this.get('userSelections');
userSelections.set('category', categoryId);
userSelections.set('subcategory', subcategoryId);
this.transitionTo('filter-categories');
},
}
});
To pass the selections to the corresponding controller, I am using a service:
// services/user-selections.js
import Ember from 'ember';
export default Ember.Service.extend({
category: null,
subcategory: null,
init() {
this._super(...arguments);
this.set('category', null);
this.set('subcategory', null);
},
});
Which is evaluated in:
// controllers/filter-categories.js
import Ember from 'ember';
export default Ember.Controller.extend({
userSelections: Ember.inject.service('user-selections'),
init() {
this._super(...arguments);
this.get('userSelections'); // We need to get it so that we can observe it?
// We can not declare the observers, because we need to make sure userSelections is first read
this.addObserver('userSelections.category', function() {
Ember.run.once(this, 'refreshProducts');
});
this.addObserver('userSelections.subcategory', function() {
Ember.run.once(this, 'refreshProducts');
});
},
actions: {
changedCategory: function(selectedCategory) {
this.set('selectedCategory', selectedCategory);
this.get('userSelections').set('category', selectedCategory.value);
},
changedSubcategory: function(selectedSubcategory) {
this.set('selectedSubcategory', selectedSubcategory);
this.get('userSelections').set('subcategory', selectedSubcategory.value);
},
},
refreshProducts: function() {
var userSelections = this.get('userSelections'),
category = userSelections.get('category'),
subcategory = userSelections.get('subcategory');
var products = this.store.filter('product', function(product) {
var catId = parseInt(product.get('category').get('id')),
subcatId = parseInt(product.get('subcategory').get('id'));
if (category && catId !== category) {
return false;
}
if (subcategory && subcatId !== subcategory) {
return false;
}
return true;
});
this.set('model', products);
},
});
Observing the userSelections (after some hacking, as seen in the comments) works: the actions are properly triggering the refreshProducts method. But it seems the method is not triggered when coming from the application route, probably because the controllers/filter-categories is not yet initialized.
(*) According to the documentation there are lots "issues" observing services.
Observers and asynchrony
Observers and object initialization
Unconsumed Computed Properties Do Not Trigger Observers
"The injected property is lazy; the service will not be instantiated until the property is explicitly called" (link)
As a result, code needs to be written in a difficult to understand way.
Is there a better pattern to share data between routes / controllers than using a service?
EDIT
These are my templates:
// partials/categories.hbs (used on the application.hbs template)
{{#each model.categories as |category| }}
<div class="categories-list row">
<div class="container">
<h3 class="category-name centered">
<span class="bg-left"></span>
<span class="bg-center uppercase">{{category.name}}</span>
<span class="bg-right"></span></h3>
</div>
<div class="category owl-carousel">
{{#each category.subcategories as |subcategory| }}
<div class="category-item">
<a href="{{subcategory.link}}">
<div class="category-icon">
<img src="{{subcategory.image}}">
</div>
<h4 class="capitalize" {{action "openSubcategory" category.id subcategory.id}}>{{subcategory.name}}</h4>
</a>
</div>
{{/each}}
</div>
</div>
{{/each}}
And:
// filter-categories.hbs
<div class="container">
<div class="row">
<div class="col-md-12">
<h2>Our Vault</h2>
<legend>Filter products by category / subcategory</legend>
</div>
</div>
<div class="row">
<div class="col-md-12">
<form class="form-inline portfolio-form">
<div class="form-group col-md-2">
{{#power-select options=optionsCategory selected=selectedCategory onchange=(action "changedCategory") as |category|}}
{{category.text}}
{{/power-select}}
</div>
<div class="form-group col-md-2">
{{#power-select options=optionsSubcategory selected=selectedSubcategory onchange=(action "changedSubcategory") as |subcategory|}}
{{subcategory.text}}
{{/power-select}}
</div>
<div class="form-group col-md-2">
<button type="button" class="btn btn-default" {{action "clearSelections" id}}><i class="fa fa-remove"></i> Clear Filters</button>
</div>
</form>
</div>
</div>
<div class="row">
{{partial "products"}}
</div>
</div>
Is there a reason you're avoiding the use of dynamic segments? If not you can pass the category and subcategory as dynamic segments when using transitionTo('filter-categories'), an added benefit would be that this route will become linkable :)
So for eg. you should define your filter-categories route like this:
// router.js
...
this.route('filter-categories', { path: 'filter-categories/:category_id/:subcategory_id' });
...
Now in your routes/filter-categories.js router you could do:
// routes/filter-categories
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
// you can put your `refreshProducts` logic in here
// (I mean in this router not literally in this method)
// and return the products model or whatever you want
// I will just return an object with the ids for simplicity
return {
categoryId: params.category_id,
subcategoryId: params.subcategory_id
};
}
...
And now in your application route:
// routes/application.js
import Ember from 'ember';
export default Ember.Route.extend({
actions: {
openSubcategory: function(categoryId, subcategoryId) {
this.transitionTo('filter-categories', categoryId, subcategoryId);
}
}
});
edit:
If you don't always have an categoryId and subcategoryId you can use query parameters instead of dynamic segments.

A property in an ember component is not propagating to its parent controller

I have a component priority-selector that looks like this...
export default Ember.Component.extend({
priority: 'low',
didInsertElement: function() {
this.$('[data-toggle="tooltip"]').tooltip();
},
actions: {
click: function(priority) {
this.set('priority', priority);
this.$('.btn-primary').removeClass('btn-primary');
this.$('.btn-for-' + priority).addClass('btn-primary');
}
}
});
Template code...
<div class="pull-right btn-group" role="group">
<div type="button" class="btn btn-default btn-for-low" {{action 'click' 'low'}}>LOW</div>
<div type="button" class="btn btn-default btn-for-medium" {{action 'click' 'medium'}}>MEDIUM</div>
<div type="button" class="btn btn-default btn-for-high" {{action 'click' 'high'}}>HIGH</div>
</div>
I use this component in a template like so...
<div class='col-sm-3'>
{{priority-selector priority=priority_gender}}
</div>
And I have it specified in the controller...
export default Ember.Controller.extend({
...
priority_gender: 'low'
...
})
But the property in the controller never changes when I interact with the component, I can observe this to be the case by looking in the ember inspector.
Your code is working.
So, make sure to specify priority_gender in right controller for template you're using priority-selector component.