I have a component which contains a switch and a yielded form.
The switch changes the model used for the yielded form.
In the said form, I have a textarea whose value comes from the model. When I update the model in the component, the textarea is bent to the correct model but the value inside it doesn't update. I can't figure out how to do it
I'm using Ember 1.13.
Here is my component:
import Ember from 'ember';
const { computed } = Ember;
export default Ember.Component.extend({
// Properties
switchField: null,
defaultModel: null,
specificModel: null,
activeModel: computed('switchField', 'defaultModel', 'specificModel', function() {
if (this.get('switchField')) {
return this.get('defaultModel');
} else {
return this.get('specificModel');
}
}),
editDisabled: computed('switchField', function() {
if (this.get('switchField')) {
return true;
} else {
return false;
}
}),
renderSwitch: function() {
Ember.run.schedule('afterRender', this, function() {
$('.toggle-switch').bootstrapToggle();
});
}.on('init'),
actions: {
reflectChange: function(value) {
this.set('switchField', value);
this.rerender();
}
}
});
The template:
<div class="col-xs-12">
<input data-toggle="toggle" data-onstyle="success" data-offstyle="danger" class="toggle-switch" type="checkbox" checked={{switchField}} onchange={{action "reflectChange" value="target.checked"}} />
<br>
<br>
</div>
{{ yield activeModel editDisabled }}
And how it is used:
{{#inherit-switch switchField=warehouse.companyInheritance.inheritCarrierProcedure defaultModel=company specificModel=warehouse as |activeModel editDisabled|}}
<div class="col-xs-12">
{{#form-group value=activeModel.errors.carrierProcedure }}
<label class="control-label" for="carrierProcedure">Procédure coursier</label>
{{textarea class="form-control" id="carrierProcedure" value=activeModel.carrierProcedure disabled=editDisabled}}
{{/form-group}}
</div>
{{/inherit-switch}}
I tried to rerender the component but it doesn't work.
I don't understand why the value doesn't change since the textarea gets correctly enabled/disabled when I toggle the switch.
Thanks for your help.
Related
I'm trying to implement a simple autosuggest in a component. I'm testing fastboot and therefore am using ember-network to communicate with my API. I'm not using ember-data right now. Whether or not this is the "ember" way to do it is a different question...I'm just trying to get this to work.
My component JS:
import Ember from 'ember';
import fetch from 'ember-network/fetch';
export default Ember.Component.extend({
searchText: null,
loadAutoComplete(query) {
let suggestCall = 'http://my.api.com/suggest?s=' + query;
return fetch(suggestCall).then(function(response) {
return response.json();
});
},
searchResults: Ember.computed('searchText', function() {
let searchText = this.get('searchText');
if (!searchText) { return; }
let searchRes = this.loadAutoComplete(searchText);
return searchRes;
})
});
And in the template:
{{input type="text" value=searchText placeholder="Search..."}}
{{ log "TEMPALTE RESULTS" searchResults }}
{{#each-in searchResults as |result value|}}
<li>{{result}} {{value}}</li>
{{/each-in}}
The template log directive is outputting this in my console:
The data is in "suggestions", so I know the fetch is working. I just can't figure out how to get at it. I can't loop over '_result'. What do I need to do to parse this and use it in a template?
Returning promise from computed property is not just straight forward, it's little tricky.
Option1. You can use ember-concurrency addon for this use case. You can look at auto complete feature explanation doc
Your component code,
import Ember from 'ember';
import { task, timeout } from 'ember-concurrency';
export default Ember.Component.extend({
searchText: null,
searchResults: task(function*(str) {
this.set('searchText', str);
let url = `http://my.api.com/suggest?s=${str}`;
let responseData = yield this.get('searchRequest').perform(url);
return responseData;
}).restartable(),
searchRequest: task(function*(url) {
let requestData;
try {
requestData = Ember.$.getJSON(url);
let result = yield requestData.promise();
return result;
} finally {
requestData.abort();
}
}).restartable(),
});
and your component hbs code,
<input type="text" value={{searchText}} onkeyup={{perform searchResults value="target.value" }}>
<div>
{{#if searchResults.isIdle}}
<ul>
{{#each searchResults.lastSuccessful.value as |data| }}
<li> {{data}} </li>
{{else}}
No result
{{/each}}
</ul>
{{else}}
Loading...
{{/if}}
</div>
Option2. You can return DS.PromiseObject or DS.PromiseArray
import Ember from 'ember';
import fetch from 'ember-network/fetch';
export default Ember.Component.extend({
searchText: null,
loadAutoComplete(query) {
let suggestCall = 'http://my.api.com/suggest?s=' + query;
return fetch(suggestCall).then(function(response) {
return response.json();
});
},
searchResults: Ember.computed('searchText', function() {
let searchText = this.get('searchText');
if (!searchText) { return; }
//if response.json returns object then you can use DS.PromiseObject, if its an array then you can use DS.PromiseArray
return DS.PromiseObject.create({
promise: this.loadAutoComplete(searchText)
});
})
});
Reference ember igniter article- The Guide to Promises in Computed Properties
First of all, IMO, it is not a good practice to call a remote call from a computed property. You should trigger it from input component/helper.
{{input type="text" value=searchText placeholder="Search..." key-up=(action loadAutoComplete)}}
And the new loadAutoComplete would be like:
loadAutoComplete(query) {
//check query is null or empty...
let suggestCall = 'http://my.api.com/suggest?s=' + query;
return fetch(suggestCall).then((response) => {
this.set('searchResults', response.json());
});
},
Your searchResults will no longer need to be a computed property. Just a property.
The following works. I can use my component to save new addresses. When the success promise is resolved, it transitions to the same route: _this.transitionToRoute('checkout.address.index')
The issue is, the form still contains the same values of the new address. I need to form to be cleared. How do I go about that?
// Controller
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
save: function(address) {
var _this = this;
this.store.createRecord('address', address.getProperties('address1', 'address2', 'city', 'postalCode')).save().then(function(){
_this.transitionToRoute('checkout.address.index');
}, function() {
// Need this promise, so we can render errors, if any, in the form
});
}
}
});
// Template
{{address-form action='save'}}
// Component object
import Ember from 'ember';
export default Ember.Component.extend({
address: function() {
return Ember.Object.create();
}.property(),
actions: {
save: function() {
this.sendAction('action', this.get('address'));
}
}
});
// Component template
<form {{action 'save' on='submit'}}>
<p>
<label>Address:
{{input value=address.address1 placeholder='11 Mars Street'}}
</label>
{{#each error in errors.address1}}
<br />{{error.message}}
{{/each}}
</p>
<p>
{{input value=address.address2 placeholder='Bel Air 1 Village'}}
{{#each error in errors.address2}}
<br />{{error.message}}
{{/each}}
</p>
<p>
<label>City:
{{input value=address.city placeholder='Makati'}}
</label>
{{#each error in errors.city}}
<br />{{error.message}}
{{/each}}
</p>
<p>
<label>Postal code:
{{input value=address.postalCode placeholder='1209'}}
</label>
{{#each error in errors.postalCode}}
<br />{{error.message}}
{{/each}}
</p>
<input type='submit' value='Next'/>
<button {{action 'cancel'}}>Cancel</button>
</form>
I'd suggest something like this (note, edited the code a little bit for readability):
export default Ember.Controller.extend({
actions: {
save: function(address, component) {
var controller = this;
var addressProperties = address.getProperties('address1', 'address2', 'city', 'postalCode');
var newAddress = controller.store.createRecord('address', addressProperties);
function onSuccess() {
controller.transitionToRoute('checkout.address.index');
component.reset());
}
function onFailure() {
// Need this promise, so we can render errors, if any, in the form
}
newAddress.save().then(onSuccess, onFailure);
}
}
});
// Component object
import Ember from 'ember';
export default Ember.Component.extend({
address: function() {
return Ember.Object.create();
}.property(),
reset: function() {
this.set('address', Ember.Object.create());
},
actions: {
save: function() {
var component = this;
component.sendAction('action', component.get('address'), component);
}
}
});
I am using a subexpression at {{input value=(cents-to-dollars model.amountInCents)}}. It is using a custom helper to convert the value from cents to dollars. My API returns cents.
However in the controllers save action, console.log(this.get('model.amountInCents')); returns undefined. Am I missing something? Maybe name or valueBinding in the input helper?
If I remove the subexpression. console.log(this.get('model.amountInCents')); outputs fine.
// Routes
import Ember from 'ember';
export default Ember.Route.extend({
model: function(params) {
return this.store.find('product', params.product_id);
}
});
// Controller
export default Ember.Controller.extend({
actions: {
save: function() {
console.log(this.get('model.amountInCents')); // returns undefined
var _this = this;
var dollars = this.get('model.amountInCents');
var amountInCents = dollars / 100;
this.get('model').set('amountInCents', amountInCents);
this.get('model').save().then(function(product){
_this.transitionToRoute('admin.products.show', product);
}, function() {
// Need this promise, so we can render errors, if any, in the form
});
return false;
},
cancel: function() {
this.transitionToRoute('products.show', this.get('model'));
}
}
});
// Template
<form {{action "save" on="submit"}}>
<p>
<label>Name:
{{input value=model.name}}
</label>
</p>
<p>
<label>Amount in cents:
{{input value=(cents-to-dollars model.amountInCents)}}
</label>
</p>
<input type="submit" value="Save"/>
<button {{action "cancel"}}>Cancel</button>
</form>
First of all, (at least in version 1.9.1) what you are proposing doesn't really work (see here - the value appears outside of the input field). The real problem, I think, is that you are not binding to a property and instead are binding to a string returned from a helper (which is not what you want).
So, what can you do?
You can set up a dollars computed property as follows:
App.IndexController = Ember.ObjectController.extend({
dollars: function(key, value){
if (arguments.length > 1) {
var dollars = value;
this.set('amountInCents', parseInt(dollars) * 100);
}
return this.get('amountInCents') / 100;
}.property('model.amountInCents')
});
Full working example here
I am still learning Ember and have encountered a problem with keep consistent behavior when showing/hidding certain elements in the template. I have the following controller
import Ember from 'ember';
export default Ember.ArrayController.extend({
actions: {
newCalendar: function() {
this.set('showCalendarForm', true);
},
hideNewCalendar: function() {
this.set('showCalendarForm', false);
this.set('calendarName', '');
},
showCalendarForm: false,
createCalendar: function() {
var name = this.get('calendarName');
if (!name) { return; }
if (!name.trim()) { return; }
var calendar = this.store.createRecord('calendar', {
name: name
});
this.set('calendarName', '');
this.set('showCalendarForm', false);
calendar.save();
},
}
});
and a template
{{#if showCalendarForm}}
<div class="input-group">
{{input
class = 'form-control'
id = 'newCalendar'
type = 'text'
placeholder = 'New calendar'
value = calendarName
autofocus = 'autofocus'
focus-out = 'hideNewCalendar'
action = 'createCalendar'
}}
</div>
{{else}}
<button class="btn btn-sm btn-primary" {{action "newCalendar"}}>New</button>
{{/if}}
Problem is that the input field only gets autofocused the first time I click the button, and on subsequent clicks, the input gets displayed, but not autofocused. How can i fix this?
I'm having the following problem.
In my app I have a screen to make a new Site. But when I save the new site via an action on the controller, the languages-property isn't sent with the POST-request to the server.
The template for adding a new Site is this:
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="name">Name</label>
<div class="controls">
{{view Ember.TextField valueBinding="name"}}
</div>
</div>
<div class="control-group">
<label class="control-label" for="languages">Languages</label>
<div class="controls">
{{view Ember.Select contentBinding="controllers.languages" selectionBinding="languages" optionValuePath="content.id" optionLabelPath="content.description" multiple="true"}}
</div>
</div>
<div class="form-actions">
<button {{ action "createSite" }} class="btn btn-primary">Save</button>
</div>
I defined my Store like this:
App.Store = DS.Store.extend({
revision : 12,
adapter : DS.RESTAdapter.extend({
namespace : 'rest'
})
});
This is my controller:
App.SitesNewController = Em.ObjectController.extend({
needs: ['languages'],
name: null,
languages: null,
createSite : function() {
var self = this;
var name = this.get('name');
var languages = this.get('languages');
// Create the new Site model
var s = App.Site.createRecord({
name : name
});
$.each(languages,function(i,lang) {
s.get('languages').addObject(lang);
});
this.get('store').commit();
}
});
This is the Site-model
App.Site = DS.Model.extend({
name : DS.attr('string'),
languages : DS.hasMany('App.Language')
});
Language-model:
App.Language = DS.Model.extend({
description : DS.attr('string')
});
The POST-request data sent to my server is this:
{
"site":{"name":"test"}
}
So I miss the language-property. Actually I expect a language_ids property with an array of id's.
When I edit my RESTAdapter-configuration like this:
DS.RESTAdapter.map('App.Site', {
languages: { embedded: 'always' }
});
Now the POST-request data is:
{
"site": {
"name":"test",
"languages":[{
"id":2,"description":"English"
},{
"id":3,"description":"Deutsch"
}]
}
}
The languages are know embedded in the request-data. This is no problem, at the backend I get the id before I save it. But know it expects the language-data to be embedded in the GET-responses also.
What is the way to send just the id's in the POST-data? I want it to be something like this:
{
"site": {
"name":"test",
"languages":[2,3]
}
}
This answer is largely derived from this other StackOverflow answer.
App.Store = DS.Store.extend({
revision : 12,
adapter : DS.RESTAdapter.extend({
namespace : 'rest',
serializer: DS.RESTSerializer.extend({
addHasMany: function (hash, record, key, relationship) {
var type = record.constructor,
name = relationship.key,
serializedHasMany = [],
manyArray, embeddedType;
embeddedType = this.embeddedType(type, name);
if (embeddedType !== 'always') { return; }
manyArray = record.get(name);
manyArray.forEach(function (record) {
serializedHasMany.push(record.get('id'));
}, this);
hash[this.singularize(key) + '_ids'] = serializedHasMany;
}
})
})
});
For reference, you might review the JSONSerializer, which the RESTSerializer mostly inherits from. The code for what addHasMany does can be found here.
Note that, for the above snippet, the only lines that really differ are the last several. Rather than serializing embedded records, ids are pushed to the hash under the singularized key (I would have used RESTSerializer#keyForHasMany if it didn't have its own check for embedded types.