Assuming an ember.js router application with an application controller, application view, a view type that I plug and play with, and an unrelated controller that handles external data. How can that third view have a computed property from the unrelated controller, what do I put into the .property() elipses so that it gets notified of changes?
e.g.
App.ExternalDataController = Em.Controller.extend
stellarProperty: 'super value' #I want OtherView to have a computer property referencing this
App.ApplicationController = Em.ArrayController.extend
content: [] #Assume a bunch of values of some kind here
App.ApplicationView = Em.View.extend
templateName: 'app-view'
App.OtherView = Em.View.extend
templateName: 'other-view'
someComputedProperty: (->
App.router.externalDataController.get('stellarProperty') + ' flying pigs'
).property() #What do I put in that property elipses?
templates
<script type="text/x-handlebars" data-template-name="application">
<div>
{{#each content}}
{{name}} -- {{view App.OtherView}}
{{/each}}
</div>
</script>
<script type="text/x-handlebars" data-template-name="other-view">
<span>Irate goats and {{view.someComputedProperty}}</span>
</script>
The short answer is that you should make the property you need available through the view's controller.
In your example, because OtherView is nested in the ApplicationView's template, it will have a controller property that is set to ApplicationController. In your router, you can call router.applicationController.connectControllers('externalData'). That will set an externalDataController property on applicationController. Then you can expose the property you need:
App.ApplicationController = Em.ArrayController.extend
content: [] #Assume a bunch of values of some kind here
externalDataController: null # set via connectControllers call in router
stellarPropertyBinding: Em.Binding.oneWay('externalDataController.stellarProperty')
And OtherView becomes:
App.OtherView = Em.View.extend
templateName: 'other-view'
someComputedProperty: (->
#get('controller.stellarProperty') + ' flying pigs'
).property('controller.stellarProperty')
Hope that helps!
Related
This question is related to this one: Ember.js {{render}} helper model not correctly set
But I think that I ask the wrong question.
Router
App.Router.map(function () {
this.resource('article', {path: '/article/:id'});
this.resource('article.new', {path: "/article/new"});
});
I have not defined a route or resource for categorynew because it is rendered as a popup within both Article and Article.new.
Template
<script type="text/x-handlebars" data-template-name="article">
{{render "category/new"}}
</script>
<!-- popups -->
<script type="text/x-handlebars" data-template-name="category/new">
Name: {{input type="text" value=name}}
Image: {{view App.UploadFile name="image" file=image }}
Category-parent: {{input value=categoryRelation}}
<button {{action 'saveCategory'}}>Save</button>
</script>
Controller
App.CategoryNewController = Ember.ObjectController.extend({
actions: {
saveCategory: function () {
var newCategory = this.store.createRecord('category', {
name: this.get('name'),
image: this.get('image'),
category_parent:this.get('category_parent')
});
newCategory.save();
console.log(this.get('naam')); // undefinded
}
}
});
When I fill the form that gets rendered with {{render category/new}} I get these errors:
Assertion failed: Cannot delegate set('name', a) to the 'content' property of object proxy <App.CategoryNewController:ember387>: its 'content' is undefined. ember-1.1.2.js:417
Uncaught Error: Object in path nam could not be found or was destroyed.
I think there must be a model in the controller. But if I do a this.get('model') it is always the wrong model. Even if I define it in App.CategoryNewRoute.
When you call render you can supply it a model, but you aren't supplying it a model. Your controller on the other hand extends ObjectController, which tells ember it's backed by a model. So either you can supply it a model, or you can change it to extend Controller (and everything will live on the controller instead of on a non-existent model).
App.CategoryNewController = Ember.Controller.extend({
name is spelled wrong in the console.log, but I'm pretty sure that's just a typo while putting it on SO.
http://emberjs.jsbin.com/EtafEFUr/1/edit
I am trying to develop my first application, and I can't get the browser to display my handlebars scripts
Here is my html :
<!doctype html>
<html>
<head>
<title>Random Presents</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<script src="lib/jquery.min.js"></script>
<script src="lib/handlebars.js"> </script>
<script src="lib/ember.js"></script>
<script src ="js/app.js"></script>
</head>
<body>
<script type="text/x-handlebars">
{{#view App.AView}}{{firstName}}{{/view}}
</script>
<script type="text/x-handlebars">
{{#view App.AView}}{{surname}}{{/view}}
</script>
</body>
</html>
and my app.js file :
App = Ember.Application.create();
App.AView = Ember.View.extend({
tagName: 'span',
firstName: 'Joe',
surname: 'Bloggs'
});
When I load the file; the page is empty, even though the source corresponds to my html file.
I don't see any error in the chrome javascript console.
Is there something really obvious that I miss?
I tested the libraries, they directly come from the website and are on last version.
Worse, I actually even tried with a script containing only html and he won't load either.
Because you created the properties in the View class, you should use the view property which is somewhat a pointer like the this keyword in some cases.
Change your template to:
<script type="text/x-handlebars">
{{#view App.AView}}
{{view.firstName}}
{{view.surname}}
{{/view}}
</script>
Because of Ember conventions, both handlebars templates from your code represent the same thing. Ember assumes the template name to be "application" when there is no name. This means that even if you fix the property to be {{view.propertyName}} on both templates, the later one will override the first (or all predecessors with the same name), because Ember will compile the templates (with Handlebars) into template functions and the name will be used as a key to a collection of templates (path Ember.TEMPLATES), so that's why you'd have move those expressions to a single template like in the code above.
But you should avoid using views like this.
Views should display data, but shouldn't keep data. Your data should live in a model (in the store) and a view should ask the controller for data, and it should get it from the sore. The controller should be populated with data from the store through the router (it knows what to do and when to do it).
I'm not saying this in a bad way at all; just trying to save you from driving on the wrong side of the street since you're starting on Ember.
It's well known that there are a lot of outdated tutorials and this causes a lot of confusion sometimes (there's a repo with outdated tutorials/articles which should be receiving notifications to update or add a disclaimar). But in general, I would suggest you to follow the guides, watch some videos about Ember, check other resources available on the internet.
Here's a commented code of a very basic sample application just to show some of the features you could and should be using:
Handlebars:
<!--
when a template doesn't have a data-template-name, Ember assumes this is the
application main template. This is usually where you'd render the layout structure
and also where you'd put the main outlet
-->
<script type="text/x-handlebars">
<h1>Example</h1>
{{outlet}}
</script>
<!--
As per convention, a named template should match with its route name
There are ways around using "partial", "render", or even defining
a View class and setting the templateName property to a different name, or
using the route's renderTemplate hook
Another thing. You can have nested views when using nested routes
This view template has another outlet to display a person from the collection
-->
<script type="text/x-handlebars" data-template-name="people">
{{#each person in controller}}
{{#linkTo people.person person}}
{{person.fullName}}
{{/linkTo}}<br />
{{/each}}
<hr />
{{outlet}}
</script>
<!--
Unlike the very first code piece in this answer, when you have a view or
template connected to a controller, you can access the data from the controller
using handlebars expressions.
-->
<script type="text/x-handlebars" data-template-name="people/person">
First name: {{view Ember.TextField valueBinding="firstName"}}<br />
Last name: {{view Ember.TextField valueBinding="lastName"}}<br />
Full Name: {{fullName}}
</script>
JavaScript:
window.App = Ember.Application.create();
// defining routes which are somewhat like states (think of a state machine)
// they also provide the ability to have hash urls
// the router is a very important piece of ember due to conventions
App.Router.map(function() {
// sample url ~/#/people
this.resource('people', function() {
// sample url ~/#/people/1
this.route('person', { path: ':person_id' });
});
});
// In this route we provide the data to the list view in "people" template
// the data will actually go to the controller 'content' property which can
// be a type of array for arraycontroller or a single object for object controller
// this should allow the view to call data from the controller
App.PeopleRoute = Em.Route.extend({
model: function() {
return App.Person.find()
}
});
// in this route we provide data for the "people/person" template
// In this case we are using the person id from the parameters to query our
// application store.
App.PeoplePersonRoute = Em.Route.extend({
model: function(params) {
return App.Person.find(params.person_id)
}
});
// This is the very first route of the application
// Most of the time, you'll simply redirect from your index to a resource
// in this example, from ~/#/ to ~/#/people
App.IndexRoute = Em.Route.extend({
redirect: function() {
this.transitionTo('people');
}
});
// The store manages your application data. Normally you only have to define
// the revision since it's not 1.0 yet (https://github.com/emberjs/data/blob/master/BREAKING_CHANGES.md)
// for this sample, I'm using the Fixture Adapter so I can add mock up data to the
// app while testing/coding front end
App.Store = DS.Store.extend({
revision: 11,
adapter: 'DS.FixtureAdapter'
});
// Using Ember-Data, you can define a Model object which uses application
// semantics to describe your data, and does many operations which you'd
// normally expect to see in a ORM. Ember-Data is no ORM, but it gets pretty close
// and in certain scenarios it goes beyond
App.Person = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string'),
fullName: function() {
return '%# %#'.fmt(
this.get('firstName'),
this.get('lastName')
);
}.property('firstName', 'lastName')
});
// Using the FixtureAdapter you can add mockup data to your data store
App.Person.FIXTURES = [
{id: 1, firstName: 'Joe', lastName: 'Bloggs'},
{id: 2, firstName: 'Other', lastName: 'Dude'}
];
// when your controller wants to handle a collection, use ArrayController
App.PeopleController = Em.ArrayController.extend();
// when it handles a single object, use ObjectController
App.PeoplePersonController = Em.ObjectController.extend();
Within a template the default context is the controller, so you need to explicitly reference the view to access its properties: {{view.property}}
In your example:
{{#view App.AView}}{{view.surname}}{{/view}}
Working example JSBin
I got confused while I was reading the explanation below on emberjs.com.
I typed the same code as the code below, but it doesn't give me the same result as the explanation shows.
I think the explanation below is omitted to some extent, so that made me misunderstand and confused.
I want to know the complete code to get the same result shown below to understand fully what the explanations means.
I really appriciate if someone could show me the complete code to get the result shown below.
Thank you very much!
As you've already seen, you can print the value of a property by enclosing it in a Handlebars expression, or a series of braces, like this:
My new car is {{color}}.
This will look up and print the View's color property. For example, if your view looks like this:
App.CarView = Ember.View.extend({
color: 'blue'
});
Your view would appear in the browser like this:
My new car is blue.
you can aloso specify global paths:
My new car is {{App.carController.color}}.
By the way, here is the code I tried, which doesn't get me the same result shown in the explation above.
/*----------
app.js
----------*/
var App = Ember.Application.create();
App.ApplicationController = Ember.Controller.extend();
App.ApplicationView = Ember.View.extend({
templateName: 'application'
});
App.CarView = Ember.View.extend({
color: 'blue',
templateName: 'car'
});
App.CarController = Ember.Controller.extend();
App.Router = Ember.Router.extend({
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/'
})
})
})
App.initialize();
/*----------
index.html
----------*/
<script type="text/x-handlebars" data-template-name="application">
<h1>Hello from Ember.js</h1>
</script>
<script type="text/x-handlebars" data-template-name="car">
My new car is {{color}}.<br />
My new car is {{App.carController.color}}.
</script>
EDIT:
index.html
<script type="text/x-handlebars" data-template-name="application">
<!-- This Works -->
{{#view App.CarView}}
(1)My new car is {{view.color}}.<br />
{{/view}}
<!-- These don't Work -->
(2)My new car is {{view.color}}.<br />
(3)My new car is {{App.CarView.color}}.<br />
(4)My new car is {{App.CarController.color}}.<br />
(5)My new car is {{App.carController.color}}.<br />
<!-- this outlet-area shows what I have in my "car" template -->
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="car">
<!-- This color property is defined in App.CarView.-->
(6)My new car is {{view.color}}.<br />
<!-- This color property is defined in App.CarCotroller.-->
(7)My new car is {{color}}.<br />
<!-- These don't work-->
(8)My new car is {{App.CarCotroller.color}}.<br />
(9)My new car is {{App.carCotroller.color}}.<br />
</script>
app.js
var App = Ember.Application.create();
App.ApplicationController = Ember.Controller.extend();
App.ApplicationView = Ember.View.extend({
templateName: 'application'
});
App.CarController = Ember.ObjectController.extend({
color:'blue'
});
App.CarView = Ember.View.extend({
color:"blue",
templateName: 'car'
});
App.Router = Ember.Router.extend({
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
connectOutlets:function(router){
router.get('applicationController').connectOutlet('car');
}
})
})
})
App.initialize();
Huh, there seems to be an error in the documentation. I will look to it, thanks for pointing it :)
Usually, when using {{color}} in the CarView template, it will lookup to the view's context, which is its controller by default. The color property should be defined in the controller.
If you want define and refer a property from the view, then you have to use the view keyword in the template. In your example, {{view.color}} should work.
EDIT: Concerning the documentation, there is huge WIP see: https://github.com/emberjs/website/tree/doc-refactor. In particular your use case is not here anymore: https://github.com/emberjs/website/blob/doc-refactor/source/guides/templates/handlebars-basics.md
UPDATE: I think all you questions here are covered in this great instructions: http://trek.github.com/.
I think it should be enough to understand your points, but I can make short answers that may help you.
1 Works because you are explicitly creating a CarView here using the {{view}} helper, so using view.color is valid.
2 Does not work because your are in the scope of the ApplicationView, which has no color property
3 Does not work because color is a property of a CarView instance not on the CarView class
4 Same as 3
5 Ember.js instantiates controllers for you, but they are not properties of the App, but they are properties of the application's router. So {{App.router.carController.color}} would work (BUT DONT USE IT, VERY BAD PRACTICE)
6 Works because your are in the CarView's template, and a color property is defined in the CarView class (and then accessible in the current CarView instance)
7 Works because it refers the color property defined in the CarController class. As I said, Ember.js instantiates the controller at application initialization time. Later in your code, when calling router.get('applicationController').connectOutlet('car'); Ember.js will create an instance of the CarView class, connect it to the router.carController instance, and display it in the {{outlet}} of the ApplicationView's template (because your are calling connectOutlet() on the applicationController. As a result, the rendering context of the CarView template is the carController, so when using {{aProperty}}, it means controller.aProperty, and in your case carController.color, which is 'blue'
8 Same as 3
9 Same as 5
For your last question, as I said, you must never access staticly to the carController instance from the templates :)
Heh, I think that's all
This is a 2 part question. 1) Is there a better way to render a model to a view asynchronously? I'm currently making the ajax request using the fetch method in the model (though I'm calling it explicitly upon initilization), then rendering the templated view using an application event, vent, which gets published from inside the model after the parse method is called. Cool but wonky? 2) Would a blocking fetch method be of use, and is it possible?
The application renders this to the page:
layout
navbar
index
Then it fetches the model and renders this:
layout
navbar
thing
1
something
somethingelse
But, if I don't use the vent trigger, it (expectedly) renders:
layout
navbar
thing
1
null
null
The html templates:
<!-- Region: NavBar -->
<script type="text/template" id="template-navbar">
<div id="navbar">
navbar
</div>
</script>
<!-- View: IndexView -->
<script type="text/template" id="template-index">
<div id="index">
index
</div>
</script>
<!-- View: ThingView -->
<script type="text/template" id="template-thing">
<div id="thing">
thing<br/>
<%= id %><br/>
<%= valOne %><br/>
<%= valTwo %><br/>
</div>
</script>
<!-- Region -->
<div id="default-region">
<!-- Layout -->
<script type="text/template" id="template-default">
layout
<div id="region-navbar">
</div>
<div id="region-content">
</div>
</script>
</div>
app.js:
window.App = { }
# Region
class RegionContainer extends Backbone.Marionette.Region
el: '#default-region'
# Called on the region when the view has been rendered
onShow: (view) ->
console.log 'onShow RegionContainer'
App.RegionContainer = RegionContainer
# Layout
class DefaultLayout extends Backbone.Marionette.Layout
template: '#template-default'
regions:
navbarRegion: '#region-navbar'
contentRegion: '#region-content'
onShow: (view) ->
console.log 'onShow DefaultLayout'
App.DefaultLayout = DefaultLayout
# NavBar (View)
class NavBar extends Backbone.Marionette.ItemView
template: '#template-navbar'
initialize: () ->
console.log 'init App.NavBar'
App.NavBar = NavBar
# Index View
class IndexView extends Backbone.Marionette.ItemView
template: '#template-index'
initialize: () ->
console.log 'init App.IndexView'
App.IndexView = IndexView
# Thing View
class ThingView extends Backbone.Marionette.ItemView
template: '#template-thing'
model: null
initialize: () ->
console.log 'init App.ThingView'
events:
'click .test_button button': 'doSomething'
doSomething: () ->
console.log 'ItemView event -> doSomething()'
App.ThingView = ThingView
# Thing Model
class Thing extends Backbone.Model
defaults:
id: null
valOne: null
valTwo: null
url: () ->
'/thing/' + #attributes.id
initialize: (item) ->
console.log 'init App.Thing'
#fetch()
parse: (resp, xhr) ->
console.log 'parse response: ' + JSON.stringify resp
# resp: {"id":"1","valOne":"something","valTwo":"somethingelse"}
#attributes.id = resp.id
#attributes.valOne = resp.valOne
#attributes.valTwo = resp.valTwo
console.log 'Thing: ' + JSON.stringify #
#
App.MyApp.vent.trigger 'thingisdone'
App.Thing = Thing
# App
$ ->
# Create application, allow for global access
MyApp = new Backbone.Marionette.Application()
App.MyApp = MyApp
# RegionContainer
regionContainer = new App.RegionContainer
# DefaultLayout
defaultLayout = new App.DefaultLayout
regionContainer.show defaultLayout
# Views
navBarView = new App.NavBar
indexView = new App.IndexView
# Show defaults
defaultLayout.navbarRegion.show navBarView
defaultLayout.contentRegion.show indexView
# Allow for global access
App.defaultRegion = regionContainer
App.defaultLayout = defaultLayout
# Set default data for MyQpp (can't be empty?)
data =
that: 'this'
# On application init...
App.MyApp.addInitializer (data) ->
console.log 'init App.MyApp'
# Test
App.modelViewTrigger = ->
console.log 'trigger ajax request via model, render view'
App.MyApp.vent.trigger 'show:thing'
App.timeoutInit = ->
console.log 'init timeout'
setTimeout 'App.modelViewTrigger()', 2000
App.timeoutInit()
# Event pub/sub handling
App.MyApp.vent.on 'show:thing', ->
console.log 'received message -> show:thing'
thing = new App.Thing(id: '1')
App.thingView = new App.ThingView(model: thing)
# I should be able to do this, but it renders null
# App.defaultLayout.contentRegion.show App.thingView
# Testing to see if I could pub from inside model..yes!
App.MyApp.vent.on 'thingisdone', ->
console.log 'received message -> thingisdone'
App.defaultLayout.contentRegion.show App.thingView
MyApp.start data
From a very basic standpoint, throwing aside the specific example that you've provided, here is how I would approach the problem and solution.
A Generic Problem / Solution
Here's a generic version of the problem:
You need to fetch a model by its id.
You need a view to render after the model has been fetched.
This is fairly simple. Attach the model to the view before fetching the data, then use the "sync" event of the model to render the view:
MyView = Backbone.View.extend({
initialize: function(){
this.model.on("sync", this.render, this);
},
render: function(){ ... }
});
myModel = new MyModel({id: someId});
new MyView({
model: myModel
});
myModel.fetch();
Things to note:
I'm setting up the model with its id, and the view with the model before calling fetch on the model. This is needed in order to prevent a race condition between loading the data and rendering the view.
I've specified generic Backbone stuff here. Marionette will generally work the same, but do the rendering for you.
Your Specific Needs
Blocking Fetch
Bad idea, all around. Don't try it.
A blocking fetch will make your application completely unresponsive until the data has returned from the server. This will manifest itself as an application that performs poorly and freezes any time the user tries to do anything.
The key to not doing this is taking advantage of events and ensuring that your events are configured before you actually make the asynchronous call, as shown in my generic example.
And don't call the fetch from within the model's initializer. That's asking for trouble as you won't be able to set up any views or events before the fetch happens. I'm pretty sure this will solve the majority of the problems you're having with the asynchronous call.
Events Between View And Model
First, I would avoid using MyApp.vent to communicate between the model and the view instance. The view already has a reference to the model, so they should communicate directly with each other.
In other words, the model should directly trigger the event and the view should listen to the event on the model. This works in the same way as my simple example, but you can have your model trigger any event you want at any time.
I would also be sure to the use bindTo feature of Marionette's views, to assist in cleaning up the events when the view is closed.
MyView = Backbone.Marionette.ItemView.extend({
initialize: function(){
this.bindTo(this.model, "do:something", this.render, this);
}
});
MyModel = Backbone.Model.extend({
doSomething: function(){
this.trigger('do:something');
}
});
myModel = new MyModel();
new MyView({
model: myModel
});
myModel.doSomething();
Other Items
There are some other items that I think are causing some problems, or leading toward odd situations that will cause problems.
For example, you have too much happening in the DOMReady event: $ ->
It's not that you have too much code being executed from this event, but you have too much code defined within this event. You should not have to do anything more than this:
$ ->
App.MyApp.start(data)
Don't define your Marionette.Application object in this event callback, either. This should be defined on its own, so that you can set up your initializers outside of the DOMReady callback, and then trigger them with the app.start() call.
Take a look at the BBCloneMail sample application for an example on rendering a layout and then populating its regions after loading data and external templates:
source: https://github.com/derickbailey/bbclonemail
live app: http://bbclonemail.heroku.com/
I don't think I'm directly answering your questions the way you might want, but the ideas that I'm presenting should lead you to the answer that you need. I hope it helps at least. :)
See Derick's new suggestion to tackle this common problem at: https://github.com/marionettejs/backbone.marionette/blob/master/upgradeGuide.md#marionetteasync-is-no-longer-supported
In short, move the asynchronous code away from your views, which means you need to provide them with models whose data has already been fetched. From the example in Marionette's upgrade guide:
Marionette.Controller.extend({
showById: function(id){
var model = new MyModel({
id: id
});
var promise = model.fetch();
$.when(promise).then(_.bind(this.showIt, this));
},
showIt: function(model){
var view = new MyView({
model: model
});
MyApp.myRegion.show(view);
}
});
Is it possible to use the subscriber/observer pattern in Ember.js? For example, view A and view B both listens to changes inside a model C. This requires model C to be able to trigger custom events. I've been trying to figure out how to make a model trigger event in Ember.js but no luck so far.
I believe the feature you are looking for is called "Bindings" in Ember.js.
There are tons of examples on the homepage that describe how to do what you are suggesting, but here is a quick recap:
window.MyApp = Ember.Application.create();
MyApp.MyModel = Ember.Object.create({
myProperty: "Hello World!",
goodbye: function() {
this.set("myProperty", "Goodbye!");
})
});
MyApp.modelInstance = MyApp.MyModel.create();
Now create your two views inside your <body> tag:
<script type="text/x-handlebars">
View1: <b>{{MyApp.modelInstance.myProperty}}</b>
</script>
<script type="text/x-handlebars">
View2: <b>{{MyApp.modelInstance.myProperty}}</b>
</script>
Now the page should render and you'll see both views say "Hello World!". Open up the console and type
MyApp.modelInstance.goodbye();
And you'll see your views change to say "Goodbye!".
The views automatically create Bindings to MyApp.modelInstance.myProperty by using the double curly braces, but you can create bindings in a variety of ways. Whenever the value of myProperty changes, all of the bindings will be automatically updated. Note, however, that you must call set("myProperty", "something new") so that Ember knows to update the bindings for you; it won't fire any change events if you just say myProperty = "something new".
At least in Sproutcore, that is what bindings are for.
If you have a model
App.Person = SC.Object.extend({
name: 'Bruce Banner'
});
You would then have a controller such as
App.personController = SC.ObjectController.create();
You could then set the content on the controller
App.personController.set('content', somePerson);
Now, any view can bind to the data on the model object.
SC.LabelView = SC.LabelView.extend({
...
valueBinding: 'App.personController.name'
})
So if you ever change the name
somePerson.set('name', 'Chris');
The view will update automatically.