Bindings for nested component not working in ember-qunit - ember.js

We have an ember component (let's call it component B), and the template for that component contains another component (component A). If we have computed properties in component B bound to properties in component A, the bindings are not working completely when we're testing using ember-qunit, but the bindings are working in the real application. In the tests, the bindings are working if we programmatically set values in components A or B, but if we use ember helpers (e.g. fillIn) to set component values, the bindings aren't getting fired. We don't experience this problem with non-nested components.
A jsfiddle that demonstrates the problem is here: http://jsfiddle.net/8WLpx/4/
Please ignore that parent component below could have just been an extension of the nested component. This is just to demonstrate the issue.
Code below if you'd rather:
HTML/handlebars
<!-- URL input -->
<script type="text/x-handlebars" data-template-name="components/url-input">
<div {{ bind-attr class=":input-group showErrors:has-error:" }}>
{{input value=web_url class="form-control"}}
</div>
</script>
<!-- video URL input -->
<script type="text/x-handlebars" data-template-name="components/video-url-input">
{{url-input class=class value=view.value selectedScheme=view.selectedScheme web_url=view.web_url}}
</script>
Component Javascript
//=============================== url input component
App.UrlInputComponent = Ember.Component.extend({
selectedScheme: 'http://',
value: function(key, value, previousValue) {
// setter
if (arguments.length > 1) {
this.breakupURL(value);
}
// getter
return this.computedValue();
}.property('selectedScheme', 'web_url'),
computedValue: function() {
var value = undefined;
var web_url = this.get('web_url');
if (web_url !== null && web_url !== undefined) {
value = this.get('selectedScheme') + web_url;
}
return value;
},
breakupURL: function(value) {
if(typeof value === 'string') {
if(value.indexOf('http://') != -1 || value.indexOf('https://') != -1) {
var results = /^\s*(https?:\/\/)(\S*)\s*$/.exec(value);
this.set('selectedScheme', results[1]);
this.set('web_url', results[2]);
} else {
this.set('web_url', value.trim());
}
}
},
onWebURLChanged: function() {
// Parse web url in case it contains the scheme
this.breakupURL(this.get('web_url'));
}.observes('web_url'),
});
//=============================== video url input component
App.VideoUrlInputComponent = Ember.Component.extend({
value: "http://",
selectedScheme: 'http://',
web_url: "",
});
Test Code
emq.moduleForComponent('video-url-input','Video URL Component', {
needs: ['component:url-input',
'template:components/url-input'],
setup: function() {
Ember.run(function() {
this.component = this.subject();
this.append();
}.bind(this));
},
});
emq.test('Test fill in url programmatically', function() {
var expectedScheme = 'https://';
var expectedWebURL = 'www.someplace.com';
var expectedURL = expectedScheme + expectedWebURL;
Ember.run(function() {
this.component.set('selectedScheme', expectedScheme);
this.component.set('web_url', expectedWebURL);
}.bind(this));
equal(this.component.get('value'), expectedURL, "URL did not match expected");
});
emq.test('Test fill in url via UI', function() {
var expectedURL = 'https://www.someplace.com';
fillIn('input', expectedURL);
andThen(function() {
equal(this.component.get('value'), expectedURL, "URL did not match expected");
}.bind(this));
});

The this.append() cannot happen in the test setup; it must happen in the "test" method because the ember qunit "test" wrapper clears all of the views before calling the standard qunit "test" method.

Related

Ember editable recursive nested components

I'm currently trying to build a component that will accept a model like this
"values": {
"value1": 234,
"valueOptions": {
"subOption1": 123,
"subOption2": 133,
"subOption3": 7432,
"valueOptions2": {
"subSubOption4": 821
}
}
}
with each object recursively creating a new component. So far I've created this branch and node components and its fine at receiving the data and displaying it but the problem I'm having is how I can edit and save the data. Each component has a different data set as it is passed down its own child object.
Js twiddle here : https://ember-twiddle.com/b7f8fa6b4c4336d40982
tree-branch component template:
{{#each children as |child|}}
{{child.name}}
{{tree-node node=child.value}}
{{/each}}
{{#each items as |item|}}
<li>{{input value=item.key}} : {{input value=item.value}} <button {{action 'save' item}}>Save</button></li>
{{/each}}
tree-branch component controller:
export default Ember.Component.extend({
tagName: 'li',
classNames: ['branch'],
items: function() {
var node = this.get('node')
var keys = Object.keys(node);
return keys.filter(function(key) {
return node[key].constructor !== Object
}).map(function(key){
return { key: key, value: node[key]};
})
}.property('node'),
children : function() {
var node = this.get('node');
var children = [];
var keys = Object.keys(node);
var branchObjectKeys = keys.filter(function(key) {
return node[key].constructor === Object
})
branchObjectKeys.forEach(function(keys) {
children.push(keys)
})
children = children.map(function(key) {
return {name:key, value: node[key]}
})
return children
}.property('node'),
actions: {
save: function(item) {
console.log(item.key, item.value);
}
}
});
tree-node component:
{{tree-branch node=node}}
Anyone who has any ideas of how I can get this working would be a major help, thanks!
Use:
save(item) {
let node = this.get('node');
if (!node || !node.hasOwnProperty(item.key)) {
return;
}
Ember.set(node, item.key, item.value);
}
See working demo.
I think this would be the perfect place to use the action helper:
In your controller define the action:
//controller
actions: {
save: function() {
this.get('tree').save();
}
}
and then pass it into your component:
{{tree-branch node=tree save=(action 'save')}}
You then pass this same action down into {{tree-branch}} and {{tree-node}} and trigger it like this:
this.attrs.save();
You can read more about actions in 2.0 here and here.

Ember-rails: function returning 'undefined' for my computed value

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!

How to do Ember integration testing for route transitions?

I'm having a problem doing integration testing with ember using Toran Billup's TDD guide.
I'm using Karma as my test runner with Qunit and Phantom JS.
I'm sure half of if has to do with my beginner's knowledge of the Ember runloop. My question is 2 parts:
1) How do I wrap a vist() test into the run loop properly?
2) How can I test for transitions? The index route ('/') should transition into a resource route called 'projects.index'.
module("Projects Integration Test:", {
setup: function() {
Ember.run(App, App.advanceReadiness);
},
teardown: function() {
App.reset();
}
});
test('Index Route Page', function(){
expect(1);
App.reset();
visit("/").then(function(){
ok(exists("*"), "Found HTML");
});
});
Thanks in advance for any pointers in the right direction.
I just pushed up an example application that does a simple transition when you hit the "/" route using ember.js RC5
https://github.com/toranb/ember-testing-example
The simple "hello world" example looks like this
1.) the template you get redirected to during the transition
<table>
{{#each person in controller}}
<tr>
<td class="name">{{person.fullName}}</td>
<td><input type="submit" class="delete" value="delete" {{action deletePerson person}} /></td>
</tr>
{{/each}}
</table>
2.) the ember.js application code
App = Ember.Application.create();
App.Router.map(function() {
this.resource("other", { path: "/" });
this.resource("people", { path: "/people" });
});
App.OtherRoute = Ember.Route.extend({
redirect: function() {
this.transitionTo('people');
}
});
App.PeopleRoute = Ember.Route.extend({
model: function() {
return App.Person.find();
}
});
App.Person = Ember.Object.extend({
firstName: '',
lastName: ''
});
App.Person.reopenClass({
people: [],
find: function() {
var self = this;
$.getJSON('/api/people', function(response) {
response.forEach(function(hash) {
var person = App.Person.create(hash);
Ember.run(self.people, self.people.pushObject, person);
});
}, this);
return this.people;
}
});
3.) the integration test looks like this
module('integration tests', {
setup: function() {
App.reset();
App.Person.people = [];
},
teardown: function() {
$.mockjaxClear();
}
});
test('ajax response with 2 people yields table with 2 rows', function() {
var json = [{firstName: "x", lastName: "y"}, {firstName: "h", lastName: "z"}];
stubEndpointForHttpRequest('/api/people', json);
visit("/").then(function() {
var rows = find("table tr").length;
equal(rows, 2, rows);
});
});
4.) the integration helper I use on most of my ember.js projects
document.write('<div id="foo"><div id="ember-testing"></div></div>');
Ember.testing = true;
App.rootElement = '#ember-testing';
App.setupForTesting();
App.injectTestHelpers();
function exists(selector) {
return !!find(selector).length;
}
function stubEndpointForHttpRequest(url, json) {
$.mockjax({
url: url,
dataType: 'json',
responseText: json
});
}
$.mockjaxSettings.logging = false;
$.mockjaxSettings.responseTime = 0;
I'm unfamiliar with Karma, but the portions of your test that needs to interact with ember should be pushed into the run loop (as you were mentioning)
Ember.run.next(function(){
//do somethin
transition stuff here etc
});
To check the current route you can steal information out of the ember out, here's some information I stole from stack overflow at some point.
var router = App.__container__.lookup("router:main"); //get the main router
var currentHandlerInfos = router.router.currentHandlerInfos; //get all handlers
var activeHandler = currentHandlerInfos[currentHandlerInfos.length - 1]; // get active handler
var activeRoute = activeHandler.handler; // active route
If you start doing controller testing, I wrote up some info on that http://discuss.emberjs.com/t/unit-testing-multiple-controllers-in-emberjs/1865

Collection of objects of multiple models as the iterable content in a template in Ember.js

I am trying to build a blog application with Ember. I have models for different types of post - article, bookmark, photo. I want to display a stream of the content created by the user for which I would need a collection of objects of all these models arranged in descending order of common attribute that they all have 'publishtime'. How to do this?
I tried something like
App.StreamRoute = Ember.Route.extend({
model: function() {
stream = App.Post.find();
stream.addObjects(App.Bookmark.find());
stream.addObjects(App.Photo.find());
return stream;
}
}
where the resource name is stream
But it doesn't work. I am using the latest released Ember 1.0.0 rc 2 and handlebars 1.0.0 rc 3 with jQuery 1.9.1 and ember-data.
Probably the way I am trying to achieve this whole thing is wrong. The problem is even if I am able to use the collection of objects of multiple models to iterate in the template, I would still need to distinguish between the type of each object to display its properties apart from the common property of 'publishtime'.
You can use a computed property to combine the various arrays and then use Javascript's built in sorting to sort the combined result.
Combining the arrays and sorting them
computed property to combine the multiple arrays:
stream: function() {
var post = this.get('post'),
bookmark = this.get('bookmark'),
photo = this.get('photo');
var stream = [];
stream.pushObjects(post);
stream.pushObjects(bookmark);
stream.pushObjects(photo);
return stream;
}.property('post.#each', 'bookmark.#each', 'photo.#each'),
example of sorting the resulting computed property containing all items:
//https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
streamSorted: function() {
var streamCopy = this.get('stream').slice(); // copy so the original doesn't change when sorting
return streamCopy.sort(function(a,b){
return a.get('publishtime') - b.get('publishtime');
});
}.property('stream.#each.publishtime')
});
rendering items based on a property or their type
I know of two ways to do this:
add a boolean property to each object and use a handlebars {{#if}} to check that property and render the correct view
extend Ember.View and use a computed property to switch which template is rendered based on which type of object is being rendered (based on Select view template by model type/object value using Ember.js)
Method 1
JS:
App.Post = Ember.Object.extend({
isPost: true
});
App.Bookmark = Ember.Object.extend({
isBookmark: true
});
App.Photo = Ember.Object.extend({
isPhoto: true
});
template:
<ul>
{{#each item in controller.stream}}
{{#if item.isPost}}
<li>post: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isBookmark}}
<li>bookmark: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isPhoto}}
<li>photo: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{/each}}
</ul>
Method 2
JS:
App.StreamItemView = Ember.View.extend({
tagName: "li",
templateName: function() {
var content = this.get('content');
if (content instanceof App.Post) {
return "StreamItemPost";
} else if (content instanceof App.Bookmark) {
return "StreamItemBookmark";
} else if (content instanceof App.Photo) {
return "StreamItemPhoto";
}
}.property(),
_templateChanged: function() {
this.rerender();
}.observes('templateName')
})
template:
<ul>
{{#each item in controller.streamSorted}}
{{view App.StreamItemView contentBinding=item}}
{{/each}}
</ul>
JSBin example - the unsorted list is rendered with method 1, and the sorted list is rendered with method 2
It's a little complicated than that, but #twinturbo's example shows nicely how to aggregate separate models into a single array.
Code showing the aggregate array proxy:
App.AggregateArrayProxy = Ember.ArrayProxy.extend({
init: function() {
this.set('content', Ember.A());
this.set('map', Ember.Map.create());
},
destroy: function() {
this.get('map').forEach(function(array, proxy) {
proxy.destroy();
});
this.super.apply(this, arguments);
},
add: function(array) {
var aggregate = this;
var proxy = Ember.ArrayProxy.create({
content: array,
contentArrayDidChange: function(array, idx, removedCount, addedCount) {
var addedObjects = array.slice(idx, idx + addedCount);
addedObjects.forEach(function(item) {
aggregate.pushObject(item);
});
},
contentArrayWillChange: function(array, idx, removedCount, addedCount) {
var removedObjects = array.slice(idx, idx + removedCount);
removedObjects.forEach(function(item) {
aggregate.removeObject(item);
});
}
});
this.get('map').set(array, proxy);
},
remove: function(array) {
var aggregate = this;
array.forEach(function(item) {
aggregate.removeObject(item);
});
this.get('map').remove(array);
}
});

Dojo Widget Templates

With reference to Simple Login implementation for Dojo MVC / - there is one point i don't understand. With regards to sample from phusick, the login dialog class does a call of dom.byId("dialog-template") - "dialog-template" is an id from the script which is the template for the dialog and should be present in an html template - not in the main html. So if I remove that, the call to dom.byId would fail
so my code structure is as follows
main.html ( calls Only main.js is called - nothing more)
main.js ( Contains the following)
require([
"dojo/_base/declare","dojo/_base/lang","dojo/on","dojo/dom","dojo/Evented",
"dojo/_base/Deferred","dojo/json","dijit/_Widget","dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin","dijit/Dialog",
"widgets/LoginDialog",
"widgets/LoginController",
"dijit/form/Form","dijit/form/ValidationTextBox","dijit/form/Button",
"dojo/domReady!"
], function(
declare,lang,on,dom,Evented,Deferred,JSON,
_Widget,
_TemplatedMixin,
_WidgetsInTemplateMixin,
Dialog,
LoginDialog,
LoginController
) {
// provide username & password in constructor
// since we do not have web service here to authenticate against
var loginController = new LoginController({username: "user", password: "user"});
var loginDialog = new LoginDialog({ controller: loginController});
loginDialog.startup();
loginDialog.show();
loginDialog.on("cancel", function() {
console.log("Login cancelled.");
});
loginDialog.on("error", function() {
console.log("Login error.");
});
loginDialog.on("success", function() {
console.log("Login success.");
console.log(JSON.stringify(this.form.get("value")));
});
});
Now LoginDialog.js and LoginDialogTemplate.html is the templatised widget for the dialog
and LoginController.js is the controller.
My LoginDialog.js is
define([
"dojo/_base/declare","dojo/_base/lang","dojo/on","dojo/dom","dojo/Evented","dojo/_base/Deferred","dojo/json",
"dijit/_Widget","dijit/_TemplatedMixin","dijit/_WidgetsInTemplateMixin",
"dijit/Dialog","dijit/form/Form","dijit/form/ValidationTextBox","dijit/form/Button",
"dojo/text!templates/loginDialogTemplate.html",
"dojo/text!templates/loginFormTemplate.html",
"dojo/domReady!"
], function(
declare,lang,on,dom,Evented,Deferred,JSON,
_Widget,
_TemplatedMixin,
_WidgetsInTemplateMixin,
Dialog,
Form,
Button,
template
) {
return declare([ Dialog, Evented], {
READY: 0,
BUSY: 1,
title: "Login Dialog",
message: "",
busyLabel: "Working...",
// Binding property values to DOM nodes in templates
// see: http://www.enterprisedojo.com/2010/10/02/lessons-in-widgetry-binding-property-values-to-dom-nodes-in-templates/
attributeMap: lang.delegate(dijit._Widget.prototype.attributeMap, {
message: {
node: "messageNode",
type: "innerHTML"
}
}),
constructor: function(/*Object*/ kwArgs) {
lang.mixin(this, kwArgs);
var dialogTemplate = dom.byId("dialog-template").textContent;
var formTemplate = dom.byId("login-form-template").textContent;
var template = lang.replace(dialogTemplate, {
form: formTemplate
});
var contentWidget = new (declare(
[_Widget, _TemplatedMixin, _WidgetsInTemplateMixin],
{
templateString: template
}
));
contentWidget.startup();
var content = this.content = contentWidget;
this.form = content.form;
// shortcuts
this.submitButton = content.submitButton;
this.cancelButton = content.cancelButton;
this.messageNode = content.messageNode;
},
postCreate: function() {
this.inherited(arguments);
this.readyState= this.READY;
this.okLabel = this.submitButton.get("label");
this.connect(this.submitButton, "onClick", "onSubmit");
this.connect(this.cancelButton, "onClick", "onCancel");
this.watch("readyState", lang.hitch(this, "_onReadyStateChange"));
this.form.watch("state", lang.hitch(this, "_onValidStateChange"));
this._onValidStateChange();
},
onSubmit: function() {
this.set("readyState", this.BUSY);
this.set("message", "");
var data = this.form.get("value");
var auth = this.controller.login(data);
Deferred.when(auth, lang.hitch(this, function(loginSuccess) {
if (loginSuccess === true) {
this.onLoginSuccess();
return;
}
this.onLoginError();
}));
},
onLoginSuccess: function() {
this.set("readyState", this.READY);
this.set("message", "Login sucessful.");
this.emit("success");
},
onLoginError: function() {
this.set("readyState", this.READY);
this.set("message", "Please try again.");
this.emit("error");
},
onCancel: function() {
this.emit("cancel");
},
_onValidStateChange: function() {
this.submitButton.set("disabled", !!this.form.get("state").length);
},
_onReadyStateChange: function() {
var isBusy = this.get("readyState") == this.BUSY;
this.submitButton.set("label", isBusy ? this.busyLabel : this.okLabel);
this.submitButton.set("disabled", isBusy);
}
});
});
My loginDialogTemplate.html is as follows
<script type="text/template" id="dialog-template">
<div style="width:300px;">
<div class="dijitDialogPaneContentArea">
<div data-dojo-attach-point="contentNode">
{form}
</div>
</div>
<div class="dijitDialogPaneActionBar">
<div
class="message"
data-dojo-attach-point="messageNode"
></div>
<button
data-dojo-type="dijit.form.Button"
data-dojo-props=""
data-dojo-attach-point="submitButton"
>
OK
</button>
<button
data-dojo-type="dijit.form.Button"
data-dojo-attach-point="cancelButton"
>
Cancel
</button>
</div>
</div>
</script>
Since the template has the id="dialog-template" so I guess when the widget calls the dom.byId("dialog-template"), it throws an error "TypeError: dom.byId(...) is null" at the line :-> var dialogTemplate = dom.byId("dialog-template").textContent;
So what am I doing wrong here?
If i use all the template scripts in the main html it works fine.
Asif,
Since you're passing in the templates in the define function, you don't need the dom.byId() to get the content. Try this:
Remove the elements from your HTML templates.
In LoginDialog.js, change your function arguments to:
...
Button,
dialogTemplate,
formTemplate
You'll need the formTemplate for the next change. I used 'dialogTemplate' instead of your 'template' so it's more obvious how it's replacing the code from the example. Next, change the beginning of the constructor to:
constructor: function(/*Object*/ kwArgs) {
lang.mixin(this, kwArgs);
//var dialogTemplate = dom.byId("dialog-template").textContent;
//var formTemplate = dom.byId("login-form-template").textContent;
var template = lang.replace(dialogTemplate, {
form: formTemplate
});
var contentWidget = new (declare(
...
I only left the commented code in so you can see what I changed. What it does is create a new template string called 'template' by substituting the {form} placeholder in your dialogTemplate HTML with the formTemplate you passed in. Then it's using that new template string to create the widget.