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.
Related
I'm having some issues with Ember Power Select. I'm able to deal with my data properly alone on the template, but for some reason power select won't let me get to any of the data from the fetch call in my component code below...
components/flight-search.js
import Component from '#ember/component';
export default Component.extend({
flightResults: null,
airResults: null,
actions: {
searchIATA(term) {
let query = `https://iatacodes.org/api/v6/autocomplete?api_key=e7c1b7cf-62fb-440c-a0ef-4facebe1ab86&query=${term}`;
return fetch(query).then(function(response) {
return response.json();
}).then(results => {
this.set('airResults', results);
});
},
}
});
components/flight-search.hbs
{{#each airResults.response.airports as |airport|}}
{{airport.name}} - {{airport.code}}
{{/each}}
{{#power-select-typeahead
search=(action "searchIATA")
triggerClass="bootstrap-theme-trigger"
dropdownClass="slide-fade bootstrap-theme-dropdown"
selected=selectedType
loadingMessage="Searching..."
placeholder="e.g. New York, NY"
onchange=(action (mut selectedType))
as |result|
}}
<div class="-detail">
{{result.response.airports.name}}
</div>
{{/power-select-typeahead}}
Notice above and outside of power select I've illustrated that I can get my data as needed.
Much appreciated, thanks!
You can try this, I have included an arrow function in both place.
searchIATA(term) {
let query = `https://iatacodes.org/api/v6/autocomplete?api_key=e7c1b7cf-62fb-440c-a0ef-4facebe1ab86&query=${term}`;
return fetch(query).then(response => response.json()).then(results => {
this.set('airResults', results);
});
}
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'm having a problem with an Ember computed property: It seems as though once the template gets updated, it stops listening to changes in the dependency property. But I don't understand why that would be the case.
Here's my template:
{{input type="text" value=searchText placeholder="Search for users..."}}
<br>
<ul>
{{#each user in searchResults}}
<li>{{user.Handle}}</li>
{{else}}
<p>No users found.</p>
{{/each}}
</ul>
And below is my controller:
App.AutocompleteController = Ember.Controller.extend({
searchText: null,
searchResults: function () {
var searchText = this.get('searchText');
var data = { 'searchTerm' : searchText };
var self = this;
alert("Calling searchResults");
if (!searchText) { return; }
if (searchText.length < 2) { return; }
$.get('/searchUsers', data).then(function (response) {
self.set("searchResults", JSON.parse(response));
}); //end then
}.property('searchText')
});
The first time searchResults actually makes an AJAX call and returns data, the autocomplete results get populated, but after that, searchResults doesn't get called again until I refresh the client.
NEVER MIND. It's right there in the code. On a successful ajax code, I'm reassigning searchResults to a static array, no longer a function.
Returning won't work out of a .then, however, so I still need a workaround for returning the data. For that, I will add a more traditional Ember event listener to call my 'search' function which will reset the property of 'searchResults'
New template:
{{input type="text" value=searchText placeholder="Search for users..." action=search on='change'}}
<ul>
{{#each user in searchResults}}
<li>{{user.Handle}}</li>
{{else}}
<p>No users found.</p>
{{/each}}
</ul>
New controller:
App.AutocompleteController = Ember.Controller.extend({
searchText: null,
search: function () {
var searchText = this.get('searchText');
var data = { 'searchTerm' : searchText };
var self = this;
if (!searchText) { return; }
if (searchText.length < 2) { return; }
else {
$.get('/searchUsers', data).then(function (response) {
self.set("searchResults", JSON.parse(response));
}); //end then
}
}.property('searchText')
});
Both functions here return 'undefined'. I can't figure out what's the problem.. It seems so straight-forward??
In the controller I set some properties to present the user with an empty textfield, to ensure they type in their own data.
Amber.ProductController = Ember.ObjectController.extend ({
quantity_property: "",
location_property: "",
employee_name_property: "",
//quantitySubtract: function() {
//return this.get('quantity') -= this.get('quantity_property');
//}.property('quantity', 'quantity_property')
quantitySubtract: Ember.computed('quantity', 'quantity_property', function() {
return this.get('quantity') - this.get('quantity_property');
});
});
Inn the route, both the employeeName and location is being set...
Amber.ProductsUpdateRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('product', params.product_id);
},
//This defines the actions that we want to expose to the template
actions: {
update: function() {
var product = this.get('currentModel');
var self = this; //ensures access to the transitionTo method inside the success (Promises) function
/* The first parameter to 'then' is the success handler where it transitions
to the list of products, and the second parameter is our failure handler:
A function that does nothing. */
product.set('employeeName', this.get('controller.employee_name_property'))
product.set('location', this.get('controller.location_property'))
product.set('quantity', this.get('controller.quantitySubtract()'))
product.save().then(
function() { self.transitionTo('products') },
function() { }
);
}
}
});
Nothing speciel in the handlebar
<h1>Produkt Forbrug</h1>
<form {{action "update" on="submit"}}>
...
<div>
<label>
Antal<br>
{{input type="text" value=quantity_property}}
</label>
{{#each error in errors.quantity}}
<p class="error">{{error.message}}</p>
{{/each}}
</div>
<button type="update">Save</button>
</form>
get rid of the ()
product.set('quantity', this.get('controller.quantitySubtract'))
And this way was fine:
quantitySubtract: function() {
return this.get('quantity') - this.get('quantity_property');
}.property('quantity', 'quantity_property')
Update:
Seeing your route, that controller wouldn't be applied to that route, it is just using a generic Ember.ObjectController.
Amber.ProductController would go to the Amber.ProductRoute
Amber.ProductUpdateController would go to the Amber.ProductUpdateRoute
If you want to reuse the controller for both routes just extend the product controller like so.
Amber.ProductController = Ember.ObjectController.extend ({
quantity_property: "",
location_property: "",
employee_name_property: "",
quantitySubtract: function() {
return this.get('quantity') - this.get('quantity_property');
}.property('quantity', 'quantity_property')
});
Amber.ProductUpdateController = Amber.ProductController.extend();
I ended up skipping the function and instead do this:
product.set('quantity',
this.get('controller.quantity') - this.get('controller.quantity_property'))
I still dont understand why I could not use that function.. I also tried to rename the controller.. but that was not the issue.. as mentioned before the other two values to fetches to the controller...
Anyways, thanks for trying to help me!
Caveat: This is part of my first ember app.
I have an Ember.MutableArray on a controller. The corresponding view has an observer that attempts to rerender the template when the array changes. All the changes on the array (via user interaction) work fine. The template is just never updated. What am I doing wrong?
I'm using Ember 1.2.0 and Ember Data 1.0.0-beta.4+canary.7af6fcb0, though I guess the latter shouldn't matter for this.
Here is the code:
var ApplicationRoute = Ember.Route.extend({
renderTemplate: function() {
this._super();
var topicsController = this.controllerFor('topics');
var topicFilterController = this.controllerFor('topic_filter');
this.render('topics', {outlet: 'topics', controller: topicsController, into: 'application'});
this.render('topic_filter', {outlet: 'topic_filter', controller: topicFilterController, into: 'application'});
},
});
module.exports = ApplicationRoute;
var TopicFilterController = Ember.Controller.extend({
topicFilters: Ember.A([ ]),
areTopicFilters: function() {
console.log('topicFilters.length -> ' + this.topicFilters.length);
return this.topicFilters.length > 0;
}.property('topicFilters'),
getTopicFilters: function() {
console.log('getTopicFilters....');
return this.store.findByIds('topic', this.topicFilters);
}.property('topicFilters'),
actions: {
addTopicFilter: function(t) {
if(this.topicFilters.indexOf(parseInt(t)) == -1) {
this.topicFilters.pushObject(parseInt(t));
}
// this.topicFilters.add(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
},
removeTopicFilter: function(t) {
this.topicFilters.removeObject(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
}
}
});
module.exports = TopicFilterController;
var TopicFilterView = Ember.View.extend({
topicFiltersObserver: function() {
console.log('from view.... topicFilters has changed');
this.rerender();
}.observes('this.controller.topicFilters.[]')
});
module.exports = TopicFilterView;
// topic_filter.hbs
{{#if areTopicFilters}}
<strong>Topic filters:</strong>
{{#each getTopicFilters}}
<a {{bind-attr href='#'}} {{action 'removeTopicFilter' id}}>{{topic}}</a>
{{/each}}
{{/if}}
var TopicsController = Ember.ArrayController.extend({
needs: ['topicFilter'],
all_topics: function() {
return this.store.find('topic');
}.property('model', 'App.Topic.#each'),
actions: {
addTopicFilter: function(t) {
App.__container__.lookup('controller:topicFilter').send('addTopicFilter', t);
}
}
});
module.exports = TopicsController;
// topics.hbs
<ul class="list-group list-unstyled">
{{#each all_topics}}
<li class="clear list-group-item">
<span class="badge">{{entryCount}}</span>
<a {{bind-attr href="#"}} {{action 'addTopicFilter' id}}>{{topic}}</a>
</li>
{{/each}}
</ul>
your observes should just be controller.topicFilters.[]
And honestly this is a very inefficient way of doing this, rerendering your entire view because a single item changed on the array. If you show your template I can give you a much better way of handling this.
Here's an example, I've changed quite a few things, and guessed on some others since I don't know exactly how your app is.
http://emberjs.jsbin.com/uFIMekOJ/1/edit