Ember bindings firing when not wanted, and not firing when required - javascript

Hey I'm having two different issues in my ember app, both of which involve bindings.
First, I have a binding firing when I don't want it to. Basically what I'm trying to achieve (I'm building a survey creator front-end app) is that when any text is entered into the 'name' field of a question, I want to add a new question object, which will render out another blank question at the end of the list of questions that the user is adding. This has the effect of there always being a new question, so an add question button is not required. The binding is working, and a new object is being added: however, since the binding is from the newest question object, the binding is triggered again when the new object is created, which in turn creates a new object, which triggers the binding again....which obviously eventually crashes the browser. I've tried using the Ember._suspendObserver function, but there isn't a lot of documentation on this, and I think I'm using it wrong - anyhow it isn't suspending the observer or pausing the binding. The observer in the code is around line 27 (contentsNameObserver)
The other issue I'm having -- I have a selection drop down box which selects what type of question the user wants (single answer, multi-choice, etc.) but the binding between the select box and the {{#each}} helper which renders the kind of question isn't triggering. I'm using the Ember.Select view helper, so there shouldn't be any issues with using get/set to fire the binding. I'm using a computed property to return an array of fields for the question type based on the value of the question type id. The computed property is in line 13 (App.SurveyContent.types), and the template templates/step3. Quick heads up that this app may be extended for more than surveys, hence 'questions' are often referred to in the code as 'content'.
I'm pretty new to ember (this is my first real app) so my code most likely has a lot of issues outside of these problems...so any comments on how I've structured my app would be hugely appreciated as well!
Javascript ember app:
App = Ember.Application.create({
rootElement: '#emberContainer'
});
App.SurveyContent = Ember.Object.extend({
name: "",
content_type: 1,
content_pos: 1,
hash: Em.A([]),
types: function() {
alert("redraw");
return App.ContentTypes[this.content_type-1].hash;
}.property()
});
App.Surveys = Ember.Object.create({
name: null,
start: $.datepicker.formatDate('mm/dd/yy' , new Date()),
end: $.datepicker.formatDate('mm/dd/yy' , new Date()),
themeID: 0,
contents: [App.SurveyContent.create()], //Pushing an instance of App.SurveyContent onto this
contentsNameObserver: function() {
context = this;
console.log("entering");
Em._suspendObserver(App.Surveys, "contents.lastObject.name", false, false, function() {
console.log("suspend handler");
context.contents.pushObject(App.SurveyContent.create());
})
}.observes("contents.lastObject.name")
});
App.ContentTypes = [
Ember.Object.create({name: 'Text question', id:1, hash: [Ember.Object.create({name: 'Question', help: 'Enter the question here', type: 'text'})]}),
Ember.Object.create({name: 'Multichoice question', id:2, hash: [Ember.Object.create({name: 'Question', help: 'Enter the question here', type: 'text'}),
Ember.Object.create({name: 'Answer', help: 'Enter possible answers here', type: 'text', multiple: true})]})
];
App.ViewTypeConvention = Ember.Mixin.create({
viewType: function() {
console.log(this);
return Em.get("Ember.TextField");
}.property().cacheable()
});
App.CRMData = Ember.Object.extend();
App.CRMData.reopenClass ({
crm_data: [],
org_data: [],
org_display_data: [],
loadData: function() {
context = this;
context.crm_data = [];
$.getJSON ("ajax/crm_data", function(data) {
data.forEach(function(crm) {
context.crm_data.pushObject(App.CRMData.create({id: crm.crm_id, name: crm.crm_name}));
crm.orgs.forEach(function(org) {
context.org_data.pushObject(App.CRMData.create({id: org.org_id, name: org.org_name, crm_id: crm.crm_id}));
}, context)
}, context)
context.updateOrganisations(5);
});
return this.crm_data;
},
updateOrganisations: function(crm_id) {
context = this;
this.org_display_data.clear();
console.log("clearing the buffer")
console.log(this.org_display_data)
context.org_data.forEach(function(org) {
if(org.crm_id == crm_id) {
context.org_display_data.pushObject(App.CRMData.create({id: org.id, name: org.name}));
}
}, context)
}
});
App.DateField = Ember.TextField.extend({
attributeBindings: ['id', 'class']
});
App.CRMSelect = Ember.Select.extend({
attributeBindings: ['id'],
change: function(evt) {
console.log(evt)
App.CRMData.updateOrganisations($('#crm').val())
}
});
App.ApplicationController = Ember.Controller.extend();
App.Step1Controller = Ember.ArrayController.extend({});
App.Step2Controller = Ember.ArrayController.extend({});
App.Step2Controller = Ember.ArrayController.extend({});
App.ApplicationView = Ember.View.extend({
templateName: 'app'
});
App.Step0View = Ember.View.extend ({
templateName: 'templates/step0'
});
App.Step1View = Ember.View.extend ({
templateName: 'templates/step1'
});
App.Step2View = Ember.View.extend ({
templateName: 'templates/step2',
didInsertElement: function() {
$( ".jquery-ui-datepicker" ).datepicker();
}
});
App.Step3View = Ember.View.extend ({
templateName: 'templates/step3',
});
App.Router = Em.Router.extend ({
enableLogging: true,
root: Em.Route.extend ({
showstep1: Ember.Route.transitionTo('step1'),
showstep2: Ember.Route.transitionTo('step2'),
showstep3: Ember.Route.transitionTo('step3'),
index: Ember.Route.extend({
route: '/',
connectOutlets: function(router){
router.get('applicationController').connectOutlet( 'step0');
}
}),
step1: Ember.Route.extend ({
route: 'step1',
connectOutlets: function(router){
router.get('applicationController').connectOutlet( 'step1', App.CRMData.loadData());
}
}),
step2: Ember.Route.extend ({
route: 'step2',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('step2')
},
}),
step3: Ember.Route.extend ({
route: 'step3',
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('step3')
},
})
})
});
Ember.LOG_BINDINGS=true;
App.LOG_BINDINGS = true;
App.ContentTypes.forEach(function(object) {
object.hash.forEach(function(hash) {
hash.reopen(App.ViewTypeConvention);
}, this);
}, this);
Html templates (I've got these in haml, so this is just a representation of the important ones)
<script type="text/x-handlebars" data-template-name="app">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="templates/step3">
<h1> Add content to {{App.Surveys.name}} </h1>
<br>
<div id = "accordion2" class = "accordion">
{{#each content in App.Surveys.contents}}
<div class="accordion-group">
<div class = "accordion-heading">
<a class = "accordion-toggle" data-parent = "#accordion2" data-toggle = "collapse" href = "#collapseOne">
{{content.name}}
</a>
</div>
<div id = "collapseOne" class = "accordion-body collapse in">
{{view Ember.TextField valueBinding="content.name" class="txtName"}}
<form class = "form-horizontal">
<div class = "accordion-inner">
<div class = "control-group">
<label class = "control-label" for ="organisation">
Content Type
<div class = "controls">
{{view Ember.Select contentBinding="App.ContentTypes" optionValuePath="content.id" optionLabelPath="content.name" valueBinding="content.content_type"}}
</div>
</div>
</div>
{{#each item in content.types }}
<div class = "control-group" >
<label class = "control-label" for = "organisation">
{{item.name}}
<div class = "controls">
{{view item.viewType }}
</div>
{{/each}}
</div>
</form>
</div>
{{/each}}
</div>
</div>
<br>
<div class = "btn" {:_action => 'showstep3'}> Next Step > </div>
</script>

I've solved the first issue, although I didn't get the suspendObserver property working I used an if statement to check the previous element, removing the infinite loop.
contentsNameObserver: function() {
context = this;
if(this.get('contents.lastObject').name) {
context.contents.pushObject(App.SurveyContent.create());
}
}.observes("contents.lastObject.name")
Any comments on how to get the _suspendObserver handler working would be appreciated though, it is something that should work but I'm doing something wrong
I've created a stripped down jsfiddle at http://jsfiddle.net/reubenposthuma/sHPv4/
It is set up to go straight to the problem step, step 3, so that I don't need to include all the previous templates.
I'm still stuck on the issue of the binding not firing though. The behaviour I'm expecting is that when the 'Content Type' dropdown box is changed, the text box underneath should change, it should re-render with two text boxes.

I realise this is an old question, but there is no documenation and precious little information I could find searching either, hence sharing what I found worked here.
What I found worked was to call Ember._suspendObserver as follows:
somePropertyDidChange: function(key) {
var that = this;
Ember._suspendObserver(this, key, null,
'somePropertyDidChange', function() {
// do stuff which would normally cause feedback loops
that.set('some.property', 'immune to feedback');
});
}.observes('some.property');
You can also use the multiple observer variant as follows:
somePropertiesDidChange: function(key) {
var that = this;
Ember._suspendObservers(this, ['some.property', 'another.property'],
null, 'somePropertiesDidChange', function() {
// do stuff which would normally cause feedback loops
that.set('some.property', 'immune to feedback');
that.set('another.property', 'also immune to feedback');
});
}.observes('some.property', 'another.property');
In my exact use case I actually called Ember._suspendObservers from an Ember.run.once() function which was setup by the observer since I wanted to make sure a number of dependant properties had settled before doing calculations which in turn would mutate some of those properties.

Related

Ember.js use itemController if not following naming Conventions

According to official documentation, way to create itemcontroller is:
App.PostsController = Ember.ArrayController.extend({
itemController: 'post'
});
App.PostController = Ember.ObjectController.extend({
// the `title` property will be proxied to the underlying post.
titleLength: function() {
return this.get('title').length;
}.property('title')
});
But I'm not setting my ArrayController to App. It is set to a local variable behind a function scope. And the itemController property can only be string (according to documentation). So how do I set the itemController property?
My code looks like this:
var Channels=Ember.Object.extend({
list:Ember.ArrayController.create(
{
"model":[
{
"id":"display",
"label":"Display",
},{
"id":"social",
"label":"Social",
},{
"id":"email",
"label":"Email",
}
]
}
)
});
App.ChannelController=Ember.Controller.extend({
channels:Channels,
}));
<script type="text/x-handlebars" data-template-name='channel'>
<div>
{{#each channel in channels.list}}
{{channel.label}}
{{/each}}
</div>
</script>
I don't want to pollute App namespace with itemControllers that is to be used locally.
Update
Suppose my channels is like this:
var Channels=Ember.Object.extend({
list:Ember.ArrayController.create(
{
"model":[
{
"id":"display",
"label":"Display",
},{
"id":"social",
"label":"Social",
},{
"id":"email",
"label":"Email",
}
]
}
),
selected:"display"
});
and I want to something like this in template:
<script type="text/x-handlebars" data-template-name='channel'>
<h1>{{channels.selected}}</h1>
<div>
{{#each channel in channels.list}}
<div {{bind-attr class="channel.isselected:active:inactive"}}>{{channel.label}}</div>
{{/each}}
</div>
</script>
so that it outputs:
<h1>display</h1>
<div>
<div class="active">Display</div>
<div class="inactive">Social</div>
<div class="inactive">Email</div>
</div>
How do I do it with components?
You'll likely want to read the guide of components to get the full picture, but the gist of it is that you want to replace all item controllers with components. However, components will also replace the template inside of the each block as well. I don't entirely understand what's going on in your code, but here's an example roughly based on your code.
// Component
App.ChannelDisplayComponent = Ember.Component.extend({
channel: null,
isSelected: function() {
// Compute this however you want
// Maybe you need to pass in another property
}.property('channel')
});
{{! Component Template }}
<div {{bind-attr class="channel.isSelected:active:inactive"}}>
{{channel.label}}
</div>
{{!Channels Template}}
{{#each channel in channels.list}}
{{channel-component channel=channel}}
{{/each}}
The component is essentially your item controller, only it gets its own template as well.
You really shouldn't be worried about polluting the app namespace (unless you're having naming collisions, but that's a different issue). And as Kitler said, you should move to components instead of item controllers. But if you want to do this, the best way I can think of is overridding the (private) controllerAt hook.
var ItemController = Ember.Controller.extend({});
App.PostsController = Ember.ArrayController.extend({
controllerAt: function(idx, object, controllerClass) {
var subControllers = this._subControllers;
if (subControllers.length > idx) {
if (subControllers[idx]) {
return subControllers[idx];
}
}
var parentController = (this._isVirtual ? this.get('parentController') : this);
var controller = ItemController.create({
target: parentController,
parentController: parentController,
model: object
});
subControllers[idx] = controller;
return controller;
}
})

Ember RSVP Hash not updating controller content

I am trying to have handle multiple controllers at one route in Ember. This response seems like the way to go, but am having difficult getting this to work. Here is a simple example that fails to work:
App.IndexRoute = Ember.Route.extend({
model: function() {
myData = [1,2,3];
return Em.RSVP.hash({
docs1: myData,
docs2: myData
});
},
setupController: function(controller, model) {
controller.set('model', model.docs1);
this.controllerFor('documents').set('model', model.docs2);
}
});
App.DocumentsController = Ember.ArrayController.extend({
count: function() {
return this.get('length');
}.property('#each'),
});
App.IndexController = Em.ArrayController.extend({
count: function() {
return this.get('length');
}.property('#each'),
});
And a JSBin showing the results:
http://jsbin.com/wavigada/1/edit
You can see that the IndexController reports the correct count of 3, but the DocumentsController doesn't get set.
Can anyone help me out?
You will need to include the documents controller whose content you populate in setupController in your IndexController via needs:
App.IndexController = Em.ArrayController.extend({
needs: ['documents'],
count: function() {
return this.get('length');
}.property('#each'),
});
Then you need to change your template to:
<script type="text/x-handlebars" data-template-name="index">
{{count}}
{{render 'documents' controllers.documents}}
</script>
Note that just putting {{render 'documents' controllers.documents}} (as you did in your question) refers to a documents property of your current model, which doesn't exists.
See: http://jsbin.com/wavigada/6/

Accessing computed properties outside of their model in ember.js

I have an ember application with a model called users.js with associated controllers and routing. In my usersController.js, I have a function which counts the number of users in the system. I can then display this figure in my users template. However, I want to display that figure in my index template instead, is this possible? How would I go about it- right now the figure doesn't seem to be available for use outside of my users model.
Here's my usersController-
App.UsersController = Ember.ArrayController.extend({
sortProperties: ['name'],
sortAscending: true,
numUsers: function() {
return this.get('model.length');
}.property('model.[]')
});
And my html-
<script type = "text/x-handlebars" id = "index">
<h2>Homepage</h2>
//This is where I would like the figure to be
<h3>There are {{numUsers}} users </h3>
</script>
<script type = "text/x-handlebars" id = "users">
<div class="col-md-2">
{{#link-to "users.create"}}<button type="button" class="btn btn-default btn-lg"><span class="glyphicon glyphicon-plus"></button> {{/link-to}}
//This works fine
<div>Users: {{numUsers}}</div>
</div>
<div class="col-md-10">
<ul class="list-group">
{{#each user in controller}}
<li class="list-group-item">
{{#link-to "user" user}}
{{user.name}}
{{/link-to}}
</li>
{{/each}}
</ul>
{{outlet}}
</div>
</script>
You can just load all users in the IndexRoute, something like this:
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('user');
}
});
And extract the shared logic, in that case user count, to a mixin, and use where needed:
App.UsersCountMixin = Ember.Mixin.create({
numUsers: function() {
return this.get('model.length');
}.property('model.[]')
});
App.IndexController = Ember.ArrayController.extend(App.UsersCountMixin, {
});
App.UsersController = Ember.ArrayController.extend(App.UsersCountMixin, {
sortProperties: ['name'],
sortAscending: true
});
So {{numUsers}} will be avaliable in your index template.
To share logic with more than one model, you will need to create some alias for model property to avoid ambiguity:
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
users: this.store.find('user'),
subjects: this.store.find('subject'),
})
}
});
App.UsersCountMixin = Ember.Mixin.create({
users: Ember.required(),
numUsers: function() {
return this.get('users.length');
}.property('users.[]')
});
App.SubjectsCountMixin = Ember.Mixin.create({
subjects: Ember.required(),
numSubjects: function() {
return this.get('subjects.length');
}.property('subjects.[]')
});
App.UsersController = Ember.ArrayController.extend(App.UsersCountMixin, {
users: Ember.computed.alias('model'),
sortProperties: ['name'],
sortAscending: true
});
App.SubjectsController = Ember.ArrayController.extend(App.SubjectsCountMixin, {
subjects: Ember.computed.alias('model'),
sortProperties: ['name'],
sortAscending: true
});
App.IndexController = Ember.ArrayController.extend(App.UsersCountMixin, App.SubjectsCountMixin, {});
Of course this is a lot of code to just show the data length, since you can just use:
<h3>There are {{users.length}} users </h3>
<h3>There are {{subjects.length}} subjecst </h3>
But I think you will have more complex computed properties to share. In that cases, mixins is a good way to achieve it.

Content not showing properly in Ember view

I'm new to Ember, and I'm following along with their Todo tutorial and making a basic app to create blog posts, adjust their code for my purposes. The app was working fine until I added an itemController to the template and a controller to handle the isCompleted event. Rather than showing the content, as it did before, it shows: <Posts.Post:ember257:1> which appears to be the model name rather than content. The Ember inspector says the model has the right attribute. It just doesn't display properly. Here's some code:
<script type="text/x-handlebars" data-template-name="posts">
<section id="postapp">
<section id="main">
<ul id="post-list">
// new code added
{{#each itemController="post"}}
<li {{bind-attr class="isCompleted:completed"}}>
{{input type="checkbox" checked=isCompleted class="toggle"}}
<label>{{title}}</label>
<p>{{content}}</p>
</li>
{{/each}}
</ul>
</section>
</section>
</script>
And the relevant JavaScript (see the bottom at PostController to see the only change after the code worked):
Posts.Post = DS.Model.extend({
title: DS.attr('string'),
content: DS.attr('string'),
isCompleted: DS.attr('boolean')
});
Posts.Post.FIXTURES = [
{
id: 1,
title: "JavaScript: The Dark Side",
content: "Here is a bunch of information on the dark side of " +
"Javascript. Welcome to hell!"
},
{
id: 2,
title: "The glory of underscore",
content: "Here, we're going to talk about the many uses of the " +
"underscore library. Read on!"
},
{
id: 3,
title: "Objectifying Objects",
content: "Objects are confusing, eh? Let's play around with objects " +
"a bit to see how to really use them."
}
];
// This is the only code that changed before the app was functioning properly
Posts.PostController = Ember.ObjectController.extend({
isCompleted: function(key, value){
var model = this.get('model');
if (value === undefined) {
// property being used as a getter
return model.get('isCompleted');
} else {
// property being used as a setter
model.set('isCompleted', value);
model.save();
return value;
}
}.property('model.isCompleted')
});
Any insight as to why the right content isn't displayed would be greatly appreciated.
I just figured out the problem. content is a property all Ember controllers, so my variable name for the post content was creating some confusion when Ember was rendering the page. When I changed the variable name in my model and other places to post_content, content was rendering properly in the page.
// template
{{#each itemController="post"}}
<li {{bind-attr class="isCompleted:completed"}}>
{{input type="checkbox" checked=isCompleted class="toggle"}}
<label>{{title}}</label>
<p>{{post_content}}</p>
</li>
{{/each}}
//model
Posts.Post = DS.Model.extend({
title: DS.attr('string'),
post_content: DS.attr('string'),
isCompleted: DS.attr('boolean')
});
And problem solved.

EmberJS how to handle transitionTo

I have the following code which calls an transitionToRoute('search') when a search-query is entered and the enter button is pressed or submit button is clicked.
However, my Router still won't show the searchQuery in the template where it says:
<p>You searched for: "{{searchQuery}}"</p>
and the URL looks like http://www.example.com/#/search/[object Object] when searching for something (which doesn't seem right to me).
(full code can be viewed over at: http://jsfiddle.net/Mn2yy/1/)
This is the relevant code:
Templates:
<script type="text/x-handlebars" data-template-name="container">
<button {{action "doSearch"}} rel="tooltip-bottom" title="search" class="icon"><i class="icofont-search"></i></button>
{{view Ember.TextField valueBinding="search" action="doSearch"}}
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="searchpage">
<h1>Search</h1>
{{#linkTo "home"}}Homepage{{/linkTo}}
<p>You searched for: "{{searchQuery}}"</p>
</script>
Application controller:
MyApp.ApplicationController = Ember.Controller.extend({
// the initial value of the `search` property
search: '',
doSearch: function() {
// the current value of the text field
var query = this.get('search');
this.transitionToRoute('search');
}
});
and the Searchpage route:
MyApp.SearchRoute = Ember.Route.extend({
setupController: function(controller) {
controller.set('searchQuery', this.get('query'));
},
renderTemplate: function() {
this.render('searchpage', { into: 'container' });
}
});
First, you need to define the dynamic segment in the router for the search route:
MyApp.Router.map(function() {
this.route("home", { path: "/" });
this.route("search", { path: "/search/:query" })
});
Then you set the searchQuery property on the application in the doSearch action. You also pass the query variable to the transitionToRoute method, since it'll fill in the dynamic segment.
MyApp.ApplicationController = Ember.Controller.extend({
// the initial value of the `search` property
search: '',
doSearch: function() {
// the current value of the text field
var query = this.get('search');
this.set('searchQuery', query);
this.transitionToRoute('search', query);
}
});
Since you need to access this property from the App.SearchController instance, you need to wire the 2 controllers together by using the needs API:
MyApp.SearchController = Ember.Controller.extend({
needs: ['application'],
application: Ember.computed.alias('controllers.application')
});
Aliased the controllers.application property to just application, to avoid too much typing eg. in the template.
Then you bind to this property in the search template:
<script type="text/x-handlebars" data-template-name="searchpage">
<h1>Search</h1>
{{#linkTo "home"}}Homepage{{/linkTo}}
<p>You searched for: "{{application.searchQuery}}"</p>
</script>
Last step: if you refresh the page at this point, searchQuery won't be automatically populated from the URL. Let's just fix that with the deserialize hook:
MyApp.SearchRoute = Ember.Route.extend({
deserialize: function(params) {
this.controllerFor('application').setProperties({
searchQuery: params.query,
search: params.query
});
},
renderTemplate: function() {
this.render('searchpage', { into: 'container' });
}
});
This will get the params from the URL and set up the application controller with the value of the query key.
That's pretty much it, hope I didn't miss anything!

Categories