I am a relative Ember newbie, so I may be missing something obvious here. I'm trying to build a custom view for a Bootstrap accordion. I'm using relative views for this, and while it is working, I'd like to understand if I can simplify a little bit. Specifically, I'd like to know if it's possible to make the href computed property of the embedded toggleView dependent on the elementId of the embedded bodyView. Here's the top level view, which is a collection:
App.AccordionView = Em.CollectionView.extend({
headingTemplateName: 'accordion-heading',
innerTemplateName: 'accordion-inner',
tagName: 'div',
classNames: ['accordion'],
contentBinding: 'controller.content',
The item view corresponds to the DOM element with the accordion-group css class.
itemViewClass: Em.View.extend({
tagName: 'div',
classNames: ['accordion-group'],
target: null,
template: Em.Handlebars.compile("{{view view.headingView contentBinding='view.content'}}{{view view.bodyView viewName='bodyViewInstance' contentBinding='view.content'}}"),
headingView: Em.View.extend({
tagName: 'div',
template: Em.Handlebars.compile("{{view view.toggleView contentBinding='view.content'}}"),
classNames: ['accordion-heading'],
toggleView: Em.View.extend({
tagName: 'div', // Can be an 'a'
templateNameBinding: 'parentView.parentView.parentView.headingTemplateName',
classNames: ['accordion-toggle'],
attributeBindings: ['dataToggle:data-toggle', 'dataParent:data-parent', 'href'],
dataToggle: 'collapse',
dataParent: function() {
return this.get('parentView.parentView.parentView.elementId');
}.property(),
Note that I've made the href property dependent on the target field in the item view.
href: function() {
return "#" + this.get('parentView.parentView.target');
}.property('parentView.parentView.target')
})
}),
bodyView: Em.View.extend({
tagName: 'div',
template: Em.Handlebars.compile("{{view view.innerView contentBinding='view.content'}}"),
classNames: ['accordion-body', 'collapse'],
This field is set when the bodyView embedded view instance is inserted into the DOM.
didInsertElement: function() {
this.get('parentView').set('target', this.get('elementId'));
},
innerView: Em.View.extend({
tagName: 'div',
classNames: ['accordion-inner'],
templateNameBinding: "parentView.parentView.parentView.innerTemplateName"
})
})
})
});
That all works. However, I'd like to do something like this in the toggleView:
href: function() {
return "#" + this.get('parentView.parentView.bodyViewInstance.elementId');
}.property('parentView.parentView.bodyViewInstance.elementId')
When I do, I get the following error, and I'm not sure I understand why:
Error: Something you did caused a view to re-render after it rendered but before it was inserted into the DOM.
Is there any way to accomplish what I'm looking to do?
Related
I'm fairly new to ember and I've been trying to tackle this problem for a couple of days but I can't seem to find a solution anywhere online.
I have a page with a list of all posts, each post has one tag (like a hashtag), either 'Fitness', 'Knowledge' or 'Social'. At the top of the page I have 3 view helpers and each view helper represents a tag (fitness, knowledge or social). These will be used to filter out the posts with that particular tag name.
My problem is that when I click on a view helper I toggle the "isSelected" property to true, which adds the "isSelected" class via classNameBindings. But when I transition to a different route on the site and come back, the "isSelected" property is reset back to false and the "isSelected" class has been removed. How do I keep these values persistent and in-tact for when I revisit the route?
Here's my code:
<script type="text/x-handlebars" data-template-name="global">
<ul class="categories">
<li>{{view App.Tag class="label fitness" text="fitness"}}</li>
<li>{{view App.Tag class="label knowledge" text="knowledge"}}</li>
<li>{{view App.Tag class="label social" text="social"}}</li>
</ul>
</script>
View:
"use strict";
App.Tag = Ember.View.extend({
tagName: 'span',
template: Ember.Handlebars.compile('{{view.text}}'),
classNames: ['label'],
classNameBindings: ['isSelected'],
isSelected: false,
click: function () {
this.toggleProperty('isSelected');
}
});
I have also tried using a controller with actions but that way persisted the "isSelected" property but didn't preserve the addition of the class when I revisited the route.
This may not be ideal, but to save the state of the application, you can put the state in the controller. You probably had a simple implementation, but maybe did not specify the isSelected as a property. The below works and you can view the jsbin here
App = Ember.Application.create();
App.Router.map(function() {
this.route('global');
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
App.GlobalController = Ember.Controller.extend({
activeTags: Ember.A()
})
App.Tag = Ember.View.extend({
tagName: 'span',
template: Ember.Handlebars.compile('{{view.text}}'),
classNames: ['label'],
classNameBindings: ['isSelected'],
isSelected: function () {
console.log("ON CHANGE", this.get('controller.activeTags'));
return this.get('controller.activeTags').contains(this.text);
}.property('controller.activeTags.#each'),
click: function () {
var tagArray = this.get('controller.activeTags');
if (tagArray.contains(this.text))
this.set('controller.activeTags', tagArray.without(this.text))
else
tagArray.pushObject(this.text);
}
});
I need to assign a static data- attribute to an Ember.View, how do I set that in the View object instead of in the {{view }} tag.
App.MessagesFormView = Ember.View.extend({
tagName: 'div',
classNames: ['modal', 'fade'],
didInsertElement: function() {
this.$().modal('show')
},
willDestroyElement: function() {
this.$().modal('hide')
},
})
Unfortunately, I don't have enough reputation to comment on Ola's answer, but I believe a slightly better way to do this is to not use a string (text in quotation marks) to denote the data attribute property name. Instead, write your property name in camelCase and Ember will automatically bind it to the hyphenated attributebinding. For example:
App.MessagesFormView = Ember.View.extend({
tagName: 'div',
attributeBindings: ['data-backdrop'],
dataBackdrop: 'static', // Binds to data-backdrop. Awesome!
});
I hope that makes sense!
This has to be done using both an attributeBindings and data-backdrop or data-whatever property of the Ember.View object.
App.MessagesFormView = Ember.View.extend({
tagName: 'div',
classNames: ['modal', 'fade'],
// Set a data attribute, of a view, requires both an attribute binding and
// an attribute assignment
attributeBindings: ['data-backdrop'],
'data-backdrop': 'static',
didInsertElement: function() {
this.$().modal('show')
},
willDestroyElement: function() {
this.$().modal('hide')
},
})
I'm having a hard time finding an answer to this, so perhaps I'm doing it all wrong. I have a collection view with an item view class. Each item has a delete button, which refers to the action "removeItem." This does remove the item, but I need to refer back to the collection view content in order to post the updated array with an API call.
App.WatchlistTilesView.Collection = Ember.CollectionView.extend({
tagName: "ul",
itemViewClass: Ember.View.extend({
attributeBindings: "role",
role: "tile",
classNames: ["tile"],
removeItem: function() {
this.remove();
},
templateStock: Ember.Handlebars.compile("
<button {{action removeItem}}>Delete</button>
...
"),
})
});
EDIT:
I found the answer to this particular problem. There was a property on the collection view that needed to be accessed by the item class. I was able to access it with this.get("parentView"). Also, I had to add the view as the target for the removeItem action.
It looks something like this:
App.WatchlistTilesView.Collection = Ember.CollectionView.extend({
tagName: "ul",
itemViewClass: Ember.View.extend({
attributeBindings: "role",
role: "tile",
classNames: ["tile"],
watchlistBinding: "parentView.watchlist"
removeItem: ->
#remove()
watchlist = #get("watchlist")
#get("controller").removeSymbol(#content, watchlist)
templateStock: Ember.Handlebars.compile("
<button {{action removeItem target="view"}}>Delete</button>
...
"),
})
});
App.WatchlistTilesController = Ember.Controller.extend({
account: null,
watchlists: null,
removeSymbol: function(entry, watchlist) {
watchlist.entries.removeObject(entry);
return WatchlistService.edit(this.account, watchlist);
}
});
I try to wrap a colorPicker component from http://www.eyecon.ro/bootstrap-colorpicker/
I need to fill the "background-color css attribute" of the template with the "value" attribute of the view...
I have tried to set {{value}} but it does not work...
Any advice ?
JavaScript:
App.ColorPicker = Em.View.extend({
classNames: 'input-append color',
attributeBindings: ['name', 'value'],
value: '',
template: Ember.Handlebars.compile('{{view Ember.TextField valueBinding="this.value"}}<span class="add-on"><i style="background-color: {{value}}"></i></span>'),
didInsertElement: function() {
$('#backgroundColor').colorpicker({format: "rgb"});
}
})
;
html:
{{view App.ColorPicker placeholder="Background color" name="backgroundColor" valueBinding="App.controller" classNames="input-large" id="backgroundColor"}}
You need to look into the {{bindAttr}} helper. There is an old but good post on it here
Basically, you can't just drop bound values into HTML tags as it then inserts the wrapper script tags and breaks everything.
What you're looking for is:
App.ColorPicker = Em.View.extend({
classNames: 'input-append color',
attributeBindings: ['name', 'value'],
value: '',
template: Ember.Handlebars.compile('{{view Ember.TextField valueBinding="this.value"}}<span class="add-on"><i {{bindAttr style="view.iStyle"}}></i></span>'),
iStyle: function() {
return "background-color:" + this.get('value');
}.property('value'),
didInsertElement: function() {
$('#backgroundColor').colorpicker({format: "rgb"});
}
});
I have an container view defined below
app.MainView = Ember.ContainerView.extend({
childViews: ['navigationView', 'gutterView', 'mainView'],
elementId: 'master',
navigationView: app.NavView,
gutterView: app.GutterView,
mainView: Ember.View.create({
elementId: 'content',
template: Ember.Handlebars.compile('{{outlet contentOutlet}}')
})
});
app.NavView = Ember.View.extend({
elementId: 'main-menu',
classNames: ['navigationPanel'],
template: Ember.Handlebars.compile('{{controller}}')
});
app.NavController = Ember.ArrayController.extend({
content: [],
});
the problem here is that when I define it like this the controller for app.NavView (app.NavController) does not get connected to the view. if I look at the {{controller}} for the NavView through the template i get the ApplicationController.
But when I define it like this:
app.MainView = Ember.ContainerView.extend({
childViews: ['navigationView', 'gutterView', 'mainView'],
elementId: 'master',
navigationView: Ember.View.extend({
elementId: 'nav',
template: Ember.Handlebars.compile('{{outlet navOutlet}}')
}),
gutterView: app.GutterView,
mainView: Ember.View.create({
elementId: 'content',
template: Ember.Handlebars.compile('{{outlet contentOutlet}}')
})
});
and connect the NavView through the connectOutlet in the router
router.get('applicationController').connectOutlet('navOutlet', 'nav');
I get that the connected controller in NavView is NavController, which is correct!
The question is, what am I missing here? I don't want an outlet here and want it to be created through the mainView.
Why is Ember not connecting the View and Controller properly when I use an ContainerView?
The "connection" between view and controller is not as "automagic" as you believed. using the outlets it's done via the call to
router.get('applicationController').connectOutlet('aOutlet', 'nav');
If you walk through the code of https://github.com/emberjs/ember.js/blob/master/packages/ember-views/lib/system/controller.js#L102 you will see that it's retrieving the controller via naming convention, and then connect the just created view to it.
If you don't want to use outlet here, I suggest you to manually give the controller to the view.
EDIT: To precise why the controller of NavView is Application in the first case:
using the NavView class directly as a child of the mainView, does not bind automatically the NavController to it. So, when you try to get it's controller, it fallbacks to its parentView's controller,.