I am using Twitter Typeahead.js in a subcomponent in Ember which I feed a dataSource function (see below).
This dataSource function queries a remote server. This query I would like to have debounced in Ember which does not seem to work.
Does this have to do with the runloop? Anything I should wrap?
import Ember from 'ember';
export default Ember.Component.extend({
dataResponse: [],
dataSource: function () {
var component = this;
// function given to typeahead.js
return function (query, cb) {
var requestFunc = function () {
var encQuery = encodeURIComponent(query);
Ember.$.getJSON('/api/autocompletion?prefix=' + encQuery).then(function (result) {
// save results
component.set('dataResponse', result.autocompletion);
// map results
var mappedResult = Ember.$.map(result.autocompletion, function (item) {
return { value: item };
});
cb(mappedResult);
});
};
// this is not debounced, why? :|
Ember.run.debounce(this, requestFunc, 500); // debounce by 500ms
};
}.property()
});
Note: I do not use Bloodhound with Typeahead.js since I need access to the results. A custom solution seemed easier at first.
Debounce works by creating a unique key based on the context/function. When you call it subsequent times it compares the existing keys to the context/function key passed in. You are passing in a different function every time you call debounce, which is why it isn't working how you're expecting it to work.
Taking the advice from #Kingpin2k I refactored the code like this:
import Ember from 'ember';
export default Ember.Component.extend({
dataResponse: [],
dataSource: function () {
var component = this;
var queryString = null;
var callBack = null;
var requestFunc = function () {
var encQuery = encodeURIComponent(queryString);
Ember.$.getJSON('/api/autocompletion?prefix=' + encQuery).then(function (result) {
// save results
component.set('dataResponse', result.autocompletion);
var mappedResult = Ember.$.map(result.autocompletion, function (item) {
return { value: item };
});
callBack(mappedResult);
});
};
// function used for typeahead
return function (q, cb) {
queryString = q;
callBack = cb;
Ember.run.debounce(this, requestFunc, 500); // debounce by 500ms
};
}.property()
});
Related
I have a use-case where I want to register routes dynamically in an initializer.
Because the application is a self-defining app I don't know the routes at development time.
Currently I created an instance-initializer:
import Ember from 'ember';
const myTempRouteList = ['home']; // this is retrieved from the backend
export function initialize(instance) {
let container = instance.container;
let router = container.lookup('router:main');
myTempRouteList.forEach(function (name) {
let routeName = name.dasherize();
router.map(function(){ // router.map is undefined here
this.resource(routeName, {path: routeName});
});
container.register(`route:${routeName}`, Ember.Route.extend({
}));
}, this);
}
export default {
name: 'register-routes',
initialize: initialize
};
The problem is that the router instance is present but is has no method map. In the documentation it is described as a public method. Some other methods I checked are present, f.i. hasRoute.
It turns out I had to call the lookupFactory method instead of the lookup method on the container.
export function initialize(instance) {
let container = instance.container;
let router = container.lookupFactory('router:main');
...
}
For people who are working on latest ember with ember-cli (Ember > 2.0). This might be helpful
//initializer.js
export function initialize(application) {
var routeNames = [];
var router = application.__container__.lookupFactory('router:main');
application.deferReadiness();
//if you want to have your custom routes on the highest level
if (routeNames.length > 0) {
router.map(function() {
var _this = this;
routeNames.forEach(function(item,index) {
_this.route(item);
});
});
}
//if you want to have your custom routes as a child of another parent route
if (routeNames.length > 0) {
router.map(function() {
this.route('parentRoute', {path:'/'}, function(){
var _this = this;
routeNames.forEach(function(item,index) {
_this.route(item);
});
});
});
}
application.advanceReadiness();
}
My app is a Fluxible / React application.
I have the following spec that attempts to test a LoginForm. Embedded components have been stubbed using rewire. I referenced http://fluxible.io/api/components.html#testing.
The first spec it("renders") passes. However, when I try to do more tests as shown in the commented code, the test fails.
I am unable to assert on LoginForm's state or trigger simulated events using TestUtils on the component. Are there any ways to do that?
import React from 'react/addons';;
import { createMockComponentContext } from 'fluxible/utils';
import createStore from 'fluxible/addons/createStore';
var rewire = require("rewire");
var rewireModule = require("../../helpers/rewire-module");
// stub inner components with LoginForm
// `rewire` instead of `require`
var LoginForm = rewire("../../../src/components/auth/login-form");
// Replace the required module with a stub component.
rewireModule(LoginForm, {
FormattedMessage: React.createClass({
render: function() { return <div />; }
}),
NavLink: React.createClass({
render: function() { return <div />; }
})
});
describe('LoginForm', function() {
var context;
var TestUtils;
var provideContext;
var connectToStores;
var MockIntlStore;
var MockAuthStore;
var noop = function(){};
var component;
beforeEach(function(){
MockIntlStore = createStore({
storeName: 'IntlStore',
getMessage: noop,
getState: function(){
return {}
}
});
MockAuthStore = createStore({
storeName: 'AuthStore'
});
context = createMockComponentContext({
stores: [MockIntlStore, MockAuthStore]
});
// React must be required after window is set
TestUtils = React.addons.TestUtils
provideContext = require('fluxible/addons/provideContext');
connectToStores = require('fluxible/addons/connectToStores');
// Wrap with context provider and store connector
LoginForm = provideContext(connectToStores(LoginForm, [MockIntlStore, MockAuthStore], function (stores) {
return {
};
}));
component = TestUtils.renderIntoDocument(
<LoginForm context={context} />
);
});
it("renders", function() {
var foundComponent = TestUtils.findRenderedDOMComponentWithClass(
component, 'login-form');
expect(foundComponent).toBeDefined();
});
// TODO fluxible wraps components so we cant reach the inner component to assert on state and trigger event handlers
// it("should have an initial state", function() {
// let initialState = {
// username: '',
// pass: ''
// }
// expect(component.state).toEqual(initialState);
// });
});
When you use provideContext and connectToStores, your component is wrapped. You have done it right to find the component using TestUtils. findRenderedDOMComponentWithClass, Simply use the foundComponent for test, that is what is being tested. i.e.
...
var foundComponent = TestUtils.findRenderedDOMComponentWithClass(
component, 'login-form');
expect(foundComponent.state).toEqual(initialState);
...
If you're still looking for a solution:
var tbg = React.createElement(x, { di: serviceLocator });
var renderer = React.addons.TestUtils.createRenderer();
var rtbg = renderer.render(tbg);
Then your method is here:
renderer._instance._instance.myMethod
Where myMethod is a function member of component x
I have a custom component that expects data and not a promise, but I am unsure if they way that I am obtaining the data is the right way.
Is this the right way to do it?
component hbs
{{x-dropdown content=salutations valuePath="id" labelPath="description" action="selectSalutation"}}
Doesn't work
controller (this is the way I expect things to work
import Ember from 'ember';
export default Ember.Controller.extend({
bindSalutations: function() {
var self = this;
this.store.find('salutation').then(function(data) {
self.set('salutations', data);
});
}.on('init'),
components/x-dropdown.js
import Ember from 'ember';
export default Ember.Component.extend({
list: function() {
var content = this.get('content');
var valuePath = this.get('valuePath');
var labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item[labelPath],
value: item[valuePath],
};
});
}.property('content'),
This works
controller
bindSalutations: function() {
var self = this;
this.store.find('salutation').then(function(data) {
self.set('salutations', data.get('content')); // pass the content instead of just the data
});
}.on('init'),
component
...
list: function() {
var content = this.get('content');
var valuePath = this.get('valuePath');
var labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item._data[labelPath], // access through the _data attribute
value: item._data[valuePath],
};
});
}.property('content'),
Ember Data returns a Proxy Promise. This means you can use the promise as if it were a collection or model itself, as long as you aren't dependent on the property being completely populated when you use it. If you really want the promise resolved, you should probably be setting it up in the route.
If you want it on your controller, you can be lazy and do it like so:
Controller
salutations: function() {
this.store.find('salutation');
}.property(),
Component
...
list: function() {
var content = this.get('content'),
valuePath = this.get('valuePath'),
labelPath = this.get('labelPath');
return content.map(function(item) {
return {
key: item.get(labelPath),
value: item.get(valuePath),
};
});
}.property('content.[]'),
Template
{{x-dropdown content=salutations valuePath="id" labelPath="description" action="selectSalutation"}}
The real trick is to watch if the collection is changing. Hence you'll see I changed the property argument to content.[]
Please look at this code...
```
App.BooksRoute = Ember.Route.extend({
model: return function () {
return this.store.find('books');
}
});
App.BooksController = Ember.ArrayController.extend({
actions: {
updateData: function () {
console.log("updateData is called!");
var books = this.filter(function () {
return true;
});
for(var i=0; i<books.length; i++) {
//doSomething…
}
}
}
});
```
I want to call the updateData action on BooksController from the outside.
I tried this code.
App.__container__.lookup("controller:books").send('updateData');
It works actually. But, in the updateData action, the this is different from the one in which updateData was called by clicking {{action 'updateData'}} on books template.
In the case of clicking {{action 'updateData'}}, the this.filter() method in updateData action will return books models.
But, In the case of calling App.__container__.lookup("controller:books").send('updateData');, the this.filter() method in updateData action will return nothing.
How do I call the updateData action on BooksController from the outside, with the same behavior by clicking {{action 'updateData'}}.
I would appreciate knowing about it.
(I'm using Ember.js 1.0.0)
You can use either bind or jQuery.proxy. bind is provided in JS since version 1.8.5, so it's pretty safe to use unless you need to support very old browsers. http://kangax.github.io/es5-compat-table/
Either way, you're basically manually scoping the this object.
So, if you have this IndexController, and you wanted to trigger raiseAlert from outside the app.
App.IndexController = Ember.ArrayController.extend({
testValue : "fooBar!",
actions : {
raiseAlert : function(source){
alert( source + " " + this.get('testValue') );
}
}
});
With bind :
function externalAlertBind(){
var controller = App.__container__.lookup("controller:index");
var boundSend = controller.send.bind(controller);
boundSend('raiseAlert','External Bind');
}
With jQuery.proxy
function externalAlertProxy(){
var controller = App.__container__.lookup("controller:index");
var proxySend = jQuery.proxy(controller.send,controller);
proxySend('raiseAlert','External Proxy');
}
Interestingly this seems to be OK without using either bind or proxy in this JSBin.
function externalAlert(){
var controller = App.__container__.lookup("controller:index");
controller.send('raiseAlert','External');
}
Here's a JSBin showing all of these: http://jsbin.com/ucanam/1080/edit
[UPDATE] : Another JSBin that calls filter in the action : http://jsbin.com/ucanam/1082/edit
[UPDATE 2] : I got things to work by looking up "controller:booksIndex" instead of "controller:books-index".
Here's a JSBin : http://jsbin.com/ICaMimo/1/edit
And the way to see it work (since the routes are weird) : http://jsbin.com/ICaMimo/1#/index
This solved my similar issue
Read more about action boubling here: http://emberjs.com/guides/templates/actions/#toc_action-bubbling
SpeedMind.ApplicationRoute = Ember.Route.extend({
actions: {
// This makes sure that all calls to the {{action 'goBack'}}
// in the end is run by the application-controllers implementation
// using the boubling action system. (controller->route->parentroutes)
goBack: function() {
this.controllerFor('application').send('goBack');
}
},
};
SpeedMind.ApplicationController = Ember.Controller.extend({
actions: {
goBack: function(){
console.log("This is the real goBack method definition!");
}
},
});
You could just have the ember action call your method rather than handling it inside of the action itself.
App.BooksController = Ember.ArrayController.extend({
actions: {
fireUpdateData: function(){
App.BooksController.updateData();
}
},
// This is outside of the action
updateData: function () {
console.log("updateData is called!");
var books = this.filter(function () {
return true;
});
for(var i=0; i<books.length; i++) {
//doSomething…
}
}
});
Now whenever you want to call updateData(), just use
App.BooksController.updateData();
Or in the case of a handlebars file
{{action "fireUpdateData"}}
This question is linked to the answer given here.
Having a checkbox in a view
App.RoleCheckbox = Em.Checkbox.extend({
userRolesBinding: 'parentView.user.roles', // Points to the roles of the user
checked: function () {
var userRoles = this.get('userRoles');
return userRoles.contains(this.get('content'));
}.property('content', 'userRoles.#each'),
click: function (evt) {
//do something
var controller = this.get("controller");
controller.clicked(evt);
}
});
I would like that the click function calls the clicked function from the RoleCheckboxController:
App.RoleCheckboxController = Em.Controller.extend({
clicked: function(evt){
//Really do the thing
}
});
But this does not work. How could I fix this ?
JSFiddle: http://jsfiddle.net/3fMpD/
You can instantiate and associate the controller to the view using the correct naming conventions.
For example, this would associate the controller to the view:
// Instead of App.RoleCheckBoxController
App.ApplicationController = Ember.Controller.extend( /* ... */ );
App.ApplicationView = Ember.View.extend( /* .. */ );
JSFiddle: http://jsfiddle.net/YL5rQ/
#c4p is definitely right and the problem there is that your controller is not being created, and furthermore App.RoleCheckbox has no way of knowing it should use App.RoleCheckboxController as its controller.
I am not quite sure if this is the most Ember-y way of doing this but you can set the controller in the init (constructor function) of the Checkbox view, and then just make sure you send to the controller all the properties it needs to work with:
App.RoleCheckbox = Em.Checkbox.extend({
init: function(){
this._super();
this.set('controller', new App.RoleController());
},
userRolesBinding: 'parentView.user.roles',
checked: function () {
var userRoles = this.get('userRoles');
return userRoles.contains(this.get('content'));
}.property('content', 'userRoles.#each'),
click: function (evt) {
this.get('controller').send('clicked',this.checked, this.content);
}
});
And the controller's code (just changing the parameters used in the function);
App.RoleCheckboxController = Em.ObjectController.extend({
clicked: function(checked,role){
var userRoles = App.User.roles;
console.log("userRoles = ", userRoles);
console.log("role = ", role);
console.log("will be: ", !checked ? "removed" : "added");
if (checked) {
userRoles.pushObject(role);
} else {
userRoles.removeObject(role);
}
console.log("updated userRoles = ", userRoles);
}
});
Working fiddle here: http://jsfiddle.net/cfSwq/3/
Hope this helps!
Your App.RoleCheckboxController is never created. The way you have things set up there will only be an instance of ApplicationController.
You can move the logic back into the view's click event to have everything work:
App.RoleCheckbox = Em.Checkbox.extend({
userRolesBinding: 'parentView.user.roles',
checked: function () {
var userRoles = this.get('userRoles');
return userRoles.contains(this.get('content'));
}.property('content', 'userRoles.#each'),
click: function (evt) {
console.log("event triggered:", evt);
//var controller = this.get("controller");
//controller.clicked(evt);
var isPresent = this.get('checked'),
userRoles = this.get('userRoles'),
role = this.get('content');
console.log("userRoles = ", userRoles);
console.log("role = ", role);
console.log("will be: ", isPresent ? "removed" : "added");
if (!isPresent) {
userRoles.pushObject(role);
} else {
userRoles.removeObject(role);
}
}
});
Updated JSFiddle