How to share settings across an application? - ember.js

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.

Related

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

Chunk Ember Model Array

I am trying to split up the model array of controller to make it easier to display but the approach I came up with is not right. How do I access the model array of a controller so that it can be manipulated and still maintain computed properties of the model?
Controller:
export default Ember.Controller.extend({
queryParams: ['page'],
page: "",
playlists: Ember.computed("model", function(){
var playlistContent = this.get("model.content");
return _.chunk(playlistContent, 3);
}),
actions: {
setPage(page){
this.set("page", page);
}
});
Template:
{{#each playlists as |playlistGroup|}}
<div class="row">
{{#each playlistGroup as |playlist|}}
<div class="col-md-4">
<div class="card playlist-card">
<img class="card-img-top" src={{playlist._data.thumbnail}} alt="Card image cap">
<div class="card-block">
<h4 class="card-title">{{playlist._data.title}}</h4>
<p class="card-text">{{playlist._data.description}}</p>
</div>
</div>
</div>
{{/each}}
</div>
{{/each}}
In Route,
setupController(controller,model){
this._super(...arguments);
controller.set('playlists',_.chunk(model, 3));
}

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'));
}
}
})
}
}
});

Using {{render}} to display a form and save a model instance from anywhere in the application

I have a resource (Trip) and its routes - trips.index, trips.edit, trips.new. I would like to put a copy of the form in trips/new into application.hbs template so it appears on every page.
The form in trips/new route works but the one in application.hbs doesn't. I get the following errors when I submit the form:
Uncaught Error: Assertion Failed: Cannot delegate set('name', a) to
the 'content' property of object proxy
: its 'content' is
undefined.
Uncaught Error: Assertion Failed: Cannot delegate set('errorMessage',
You have to fill all the fields) to the 'content' property of object
proxy : its 'content' is
undefined.
I have the following code.
application.hbs:
...
{{render "trips/new"}}
...
{{outlet}}
...
templates/trips/new.hbs:
<form {{action "save" on="submit"}} role="form">
<p class="text-danger">{{errorMessage}}</p>
<div class="form-group">
<label for="">Name</label>
{{input class="form-control" value=name}}
</div>
<div class="form-group">
<label for="">Country</label>
{{input class="form-control" value=country}}
</div>
<div class="form-group">
<label for="">Start Date</label>
{{input class="form-control" value=startDate placeholder="YYYY-MM-DD"}}
</div>
<div class="form-group">
<label for="">End Date</label>
{{input class="form-control" value=endDate placeholder="YYYY-MM-DD"}}
</div>
<input type="submit" value="Save" class="btn btn-primary">
<button {{action "cancel"}} class="btn btn-default">Cancel</button>
</form>
controllers/trips/base.js:
import Ember from 'ember';
export default Ember.ObjectController.extend({
isValid: Ember.computed(
'name',
'country',
function() {
return !Ember.isEmpty(this.get('name')) &&
!Ember.isEmpty(this.get('country'));
}
),
actions: {
save: function() {
if (this.get('isValid')) {
var _this = this;
this.get('model').save().then(function(trip) {
_this.transitionToRoute('trips.show', trip);
});
} else {
this.set('errorMessage', 'You have to fill all the fields');
}
},
cancel: function() {
return true;
}
}
});
controllers/trips/new.js:
import TripsBaseController from './base';
export default TripsBaseController.extend({
actions: {
cancel: function() {
this.transitionToRoute('trips.index');
return false;
}
}
});
routes/trips/new.js:
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.createRecord('trip');
},
actions: {
save: function() {
return true;
},
cancel: function() {
return true;
}
}
});
Any idea how I can solve this? I'm about to try converting it into a View or component but I'm wondering if I can still use {{render}} and I'm missing something simple here.
I'm using ember-cli with ember 1.7.0 and ember-data 1.0.0-beta.10.
I've also added a JS Bin here:
http://jsbin.com/zofive/edit
Here's the answer - I needed to set the model using the Application Route, setupController and controllerFor.
import Ember from 'ember';
export default Ember.Route.extend({
setupController: function() {
this.controllerFor('trips/new').set('model', this.store.createRecord('trip'));
}
});
JS Bin (non-ES6): http://jsbin.com/zofive/13
Thanks to #locks, #abuiles and #xymbol.

Why would a newly created record not show up in a list?

I have a form that creates a record, then transitions to the list of resources for a particular object. However once the record is created, it is not reflected in the list of resources. If I refresh the page, the record is saved in the correct place. I have the ember chrome extension installed and if I look under Resources, then the resource is there pointing to the correct Badge. But if I go to badge first, and look for resources, it is not listed. Any ideas? I would be happy to provide any more information necessary to clarify. Thank you in advance
Create Resource Form Controller and Route
Controller
App.ResourcesCreateController = Ember.ObjectController.extend({
resourceTypes: ["link","file","video"],
needs: ['badge','resourcesIndex'],
actions: {
save: function() {
//Gather the info from the form
var description = this.get('description');
var url = this.get('url');
var type = this.get('type');
var text = this.get('text');
var badge = this.get('controllers.badge').get('model');
//set the data to the model of the route (ResourceCreateRoute)
var resource = this.get('model');
console.log(resource);
resource.set('description',description);
resource.set('url',url);
resource.set('type',type);
resource.set('text',text);
resource.set('badge',badge);
var self = this;
//save the route
var a = resource.save().then(function() {
//if success
//this.get('store').reload();
console.log('%c that resource saved rather nicely','color:green;');
self.transitionToRoute('resources.index',self.badge);
}, function() {
//if failure
console.log('%c Yea boss...that didnt go so hot', 'color:red;');
self.set('isError',true);
});
},
reset: function() {
this.transitionToRoute('resources.index');
}
}
});
Route
App.ResourcesCreateRoute = Ember.Route.extend({
model: function() {
return this.store.createRecord('resource');
}
})
List Resources Route
App.ResourcesRoute = Ember.Route.extend({
model: function(){
return this.modelFor('badge').get('resources');
}
});
Models
Resource Model
App.Resource = DS.Model.extend({
'badge': DS.belongsTo('badge'),
'text': attr('string'),
'url': attr('string'),
'description': attr('string'),
'type': attr('string')
});
Badge Model
App.Badge = DS.Model.extend({
'category': DS.belongsTo('category'),
'title': attr('string'),
'type': attr('string'),
'ord': attr('number'),
'short_description': attr('string'),
'full_description': attr('string'),
'mentor': DS.belongsTo('employee'),
'parent':DS.belongsTo('badge'),
'icon': attr('string'),
'required': attr('boolean'),
'signoff_role': attr('string'),
'created_at': attr('string'),
'updated_at': attr('string'),
'resources': DS.hasMany('resource', { async: true } ),
'quiz': DS.belongsTo('quiz', { async: true } )
});
Templates
List of Resources
{{#link-to "resources.create" class="btn btn-primary btn-xs pull-right"}} Create Resource {{icon "plus"}}{{/link-to}}
<h3>Resources</h3>
<dl>
{{#each resource in controller}}
{{render resources/resource resource}}
{{else}}
<p class="lead text-muted">There are no resources</p>
{{/each}}
</dl>
Resource Item Template
{{#if isEditing}}
<div {{bindAttr class="controller.isError:alert-danger:alert-info :alert"}}>
<div class="row">
<div class="col col-lg-2">
<small>Type</small>
{{view Ember.Select contentBinding="resourceTypes" classNames="form-control" valueBinding="type"}}
</div>
<div class="col col-lg-10">
<small>Resource Name</small>
{{input valueBinding="text" class="form-control"}}
</div>
</div>
<div class="row">
<div class="col col-lg-12">
<br>
<small>Description</small>
{{textarea valueBinding="description" rows="5" class="form-control"}}
</div>
</div>
<div class="row">
<div class="col col-lg-12">
<br>
<small>URL,File Name, or Vimeo ID</small>
{{input valueBinding="url" class="form-control"}}
</div>
</div>
<hr>
<div class="btn-group">
<div {{action "save"}} class="btn btn-primary">{{icon "floppy-save"}} Save</div>
{{#if confirmDelete}}
<div {{action "delete"}} class="btn btn-danger">{{icon "trash"}} Are You sure?</div>
{{else}}
<div {{action "confirm"}} class="btn btn-danger">{{icon "trash"}} Delete</div>
{{/if}}
</div>
<div {{action "reset"}} class="btn btn-default"> {{icon "ban-circle"}} Cancel</div>
</div>
{{else}}
<div class="btn-group pull-right btn-group-xs">
{{#if view.hover }}
<div {{action "edit"}} class="btn btn-default">{{icon "cog"}}</div>
{{/if}}
</div>
<dt>
<span class="text-muted">{{resource_icon type}}</span> {{text}}
</dt>
{{#if description}}
<dd class="text-muted" style="margin-bottom:1em">
{{markdown description}}
</dd>
{{/if}}
<hr>
{{/if}}
Create Resource Template
<h3>Create Resource</h3>
<div class="row">
<div class="col col-lg-2">
<small>Type</small>
{{view Ember.Select contentBinding="resourceTypes" classNames="form-control" valueBinding="type"}}
</div>
<div class="col col-lg-10">
<small>Resource Name</small>
{{input valueBinding="text" class="form-control"}}
</div>
</div>
<div class="row">
<div class="col col-lg-12">
<br>
<small>Description</small>
{{textarea valueBinding="description" rows="5" class="form-control"}}
</div>
</div>
<div class="row">
<div class="col col-lg-12">
<br>
<small>URL,File Name, or Vimeo ID</small>
{{input valueBinding="url" class="form-control"}}
</div>
</div>
<hr>
<div {{action "save"}} class="btn btn-primary">{{icon "floppy-save"}} Save</div>
<div {{action "test"}} class="btn btn">Test</div>
{{#link-to "resources.index" class="btn btn-default" }} {{icon "ban-circle"}} Cancel {{/link-to}}
<br><br>
</div>
Just some general notes first.
With this much code, everyone's going to have a much easier time helping you if you provide a JSBin or something. It's a bit of extra work for you, but you're asking for help, and this is a lot to just mentally parse and run. Personally, it was some extra overhead for me because you didn't include your router, so I had to do a pass just to try to figure out how badge and resource were related.
When you're using an ObjectController with the route model set to a new record, with input helpers, you shouldn't need to do all of that setting. That's why you specified those value bindings on the helpers. But when you do need to set a bunch of properties, you can just do that all at once with something like record.setProperties({prop1: prop1Value, prop2: prop2Value ...}); and save yourself a lot of typing.
I don't understand why you're using resourcesIndex as a ResourcesCreateController need. To actually answer your question, it might work to specify just 'resources' as a need
then use something like
resource.save().then(function(record){
self.get("controllers.resources").pushObject(record);
self.transitionToRoute("resources.index", badge); // I don't know if this makes any sense because you didn't show your router, but at the very least, don't use self.badge, or even self.get("badge"), because badge is already accessible in this scope.
}
It'd be nice to see your Model definitions, and even better if you had a jsbin setup showing the problem.
You could always try hooking it up on createRecord.
App.ResourcesCreateRoute = Ember.Route.extend({
model: function() {
return this.store.createRecord('resource', {badge: this.modelFor('badge')});
}
})
It seems like when you create the new resource, you aren't putting it in the store. You should try something like this.store.createRecord(resource) then instead of your resource.save, do a this.store.commit.
I'm not entirely sure I'm right. But it may be a possibility.
Fixed. I am not sure if this is the correct way to handle this but basically the parent model needed to be reloaded once the child model was created and saved. So like this
save: function() {
var self = this;
var resource = this.store.createRecord('resource',{
property: 'property',
relatedProperty: this.get('model'),
//etc
});
resource.save().then(function(){
self.get('model').reload();
},function(){
//do stuff because the resource didnt save
});
}