In my backbone app I create a popover in parent view and add it to the DOM like this
afterRender: function() {
this.$el.append(
new Popover.Views.Default({
stick:'right',
offsetTop: 3,
offsetRight: 5,
content: "Foo",
reference: this.$el
}).render().$el
);
},
toggle: function(){
app.vent.trigger('popover34:toggle');
}
However the popover is appended to its parent view. The parent view is a link and this causes some css issues in the popover.
My question is, is it safe to do something like:
afterRender: function() {
$('body').append(
new Popover.Views.Default({
stick:'right',
offsetTop: 3,
offsetRight: 5,
content: "Foo",
reference: this.$el
}).render().$el
);
}
Or is there a way to set it just beside the parent view? The problem is that when I toggle the popover it will be added to the DOM multiple times.
To second (and hopefully clarify) Isaac's answer here, your 'parent' view should be in one region, the Popover view should be in another region.
Brian Mann has done an excellent set of videos on Marionette.js and good application design, one specifically that demonstrates your exact scenario.
Good luck,
Aaron
It looks like you're using Marionette. Using Marionette, one way to handle this is with regions. From the docs:
Regions provide a consistent way to manage your views and show / close them in your application. They use a jQuery selector to show your views in the correct place.
You can put your region anywhere you like and trigger it using the Event Aggregator. In addition, it automatically closes old views so you don't end up with zombies.
Related
A question regarding transclude within an angular 1.5.8 component, and it's uses.
Here is an example of some code;
var app = angular.module('app', [])
function AccordionController () {
var self = this;
// add panel
self.addPanel = function(panel) {
// code to add panel
}
self.selectPanel = function() {
//code to select panel
}
}
// register the accordion component
app.component('accordion', {
template: '<!---accordion-template-->',
controller: AccordionController
}
function AccordionPanelController () {
// use parents methods here
var self = this;
// add panel
self.parent.addPanel(self);
// select panel
self.parent.selectPanel(self);
}
// register the accordion-panel component
app.component('accordionPanel', {
// require the parent component
// In this case parent is an instance of accordion component
require: {
'parent': '^accordion',
template: '<!---accrodion-panel-template-->',
controller: AccordionController
}
My question is would it be better to nest all the according panels within the parent using transclude or alternatively pass in a data array to the parent which this loops out the required number of panels based on the array passed inside using a binding.
Thanks
// added
Many thanks for your reply, an example I have of transclude possibly being necessary is in the following bit of code
<modal modal-id="editCompany" title="Edit Company"> <company-form company="$ctrl.company"></company-form> </modal>
Here we have a modal component which may have a variety of other components used within it, on the example above I am adding the company form, but this could we be an contact form. is there an alternative way?
I've worked with angular pretty extensively. Two enterprise tools managing and displaying large amounts of data, dozens of interactive widget modules, all that.
Never, once, have I had anything to do with transclude. At work we are explicitly told not to use it (link functions too). I thought this was a good thing, and the way Angular 2 turned out it seemed that thinking wasn't totally without reason.
I would go with the iteration to lay out the required number of items. At work this wouldn't be a choice because transclude wouldn't be an option.
The thing with using transclude in a component architecture is that it visually breaks the idea of single responsibility and messes with the architecture.
<html>
<navigation></navigation>
<accordion>
<accordion-panel></accordion-panel>
</accordion>
<footer></footer>
</html>
In this example you know your page has a navigation menu, an accordion and a footer. But at the index level (or root component) you don't want to know / see what the accordion contains.
So the accordion-panel component should only appear in its direct parent component.
As for your other question, through the use of require or attributes you pass an array of panels that you iterate using ng-repeat inside the accordion component.
app.component('accordion', {
template: `<accordion-panel ng-repeat="..."></accordion-panel>`,
controller: AccordionController
}
In our rental application, we make an API call that populates an array, thus triggering an ngRepeat. This creates a list of divs that show basic information about the rental property.
Clicking on a property expands the div and then another API call is made to populate an interior ngRepeat with a list of tenants. Some of these properties have up to 100 tenants listed (past and present are included). The tenant divs themselves are also expandable, and this reveals the majority of the functionality. You can download rental agreements, view history, etc. All of this functionality is a single directive made up of a number of ng-includes.
If you're still following, there's an outside ngRepeat and an interior ngRepeat with a huge directive inside of it.
<div ng-repeat="properties in property_collection track by property.ID>
*code removed*
<div ng-repeat="tenant in property_tenant_collection track by tenant.ID>
*...code here...*
<div tenant-menu></div>
The directive tenant-menu and all of the ngIncludes and watchers that come with it are rendered when you expand the list of properties. They just aren't visible yet. Clicking on the tenant in the list just changes the height of the div, thus revealing the interior menu.
The performance implications of the way this UI was designed are absolutely dire. There's over 15,000 watchers on the page for elements that you can't even see. Taking action on one tenant fires the digest loop for all of them for no reason. It takes under a second to retrieve the necessary data, but almost 20 seconds to render a list of 60 applicants. I've removed the directive completely (which means nothing happens when you click on a tenant) and the loading & rendering time goes from 20 seconds to 2-3 seconds.
I'm not sure how I can achieve this, but is it possible to defer attaching this directive until the tenant is clicked? I don't want clicking on the tenant div to just change the height and reveal what's inside, I want to append the entire directive right then and THEN expand the height. Ideally, when the click event is fired again and the tenant is collapsed I would also be able to destroy any watchers and clean up after myself.
Whew.
Edit: The infamous sliding directive is pasted below. I think it's strange that a click event is being bound inside, but perhaps this is an O.K. code pattern in Angular. I have a hunch that I may be able to leverage the compile, postlink, and prelink functions along with the accepted answer. I inherited a bit of legacy code I think.
angular.module('jnjManagement.directives').directive('slideableTenant', function($compile, tenantService) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
var targetX, contentX;
attrs.expanded = false;
element.bind('click', function() {
if (!targetX) targetX = document.querySelector(attrs.slideToggleTenant);
if (!contentX) contentX = targetX.querySelector('.slideable_tenant_content');
if (!attrs.expanded) {
contentX.style.border = '1px solid rgba(0,0,0,0)';
var y = contentX.clientHeight;
contentX.style.border = 0;
targetX.style.height = 'auto';
} else {
targetX.style.height = '0px';
}
attrs.expanded = !attrs.expanded;
});
}
};
});
Ideally you would have a property directive and a tenant directive and you would indeed only load the content of the expanded element when it was clicked. I would make use of Angular's $compile.
$scope.clickHandler = function(){
$('expanded-element').append($compile('<detail-directive></detail-directive')($scope));
};
Of course you would want to clean out elements when they are collapsed, too, but it sounds like you have a good grasp of performance considerations.
I'm new to ember and am struggeling with the typical "how would one do that"-Problem. What I've got is fairly simple and I know how to do it, but my way is so complicated that I do not think it's correct.
The case:
<ul>
<li>{{link-to top-level}}</li>
<li>{{link-to another-top-level</li>
<ul class="submenu">
<li>{{link-to submenu</li>
</ul>
</ul>
What should happen is:
When a route is clicked, the corresponding list element should become active.
When a submenu is clicked the corresponding upper ul-element should get the class open
It's a fairly simple case with jQuery, but I understand that this is not scalable and abstracted and stuff.
Therefore I started with this approach:
Create a controller / template construct for the entire navigation to handle it's state (there are some other things I need to check as well, so it came in handy).
since ember adds the active class to the anchor tag I created a component to observe that:
Like:
export default Ember.Component.extend({
tagName: 'li',
classNameBindings: ['active'],
active: function() {
return this.get('childViews').anyBy('active');
}.property('childViews.#each.active')
});
Replacing the li elements with {{linked-list}} does indeed work.
But what next? Do I need to add another component to watch the component to watch the build in behaviour of active links? Do I have to write dedicated MVC-Classes for all the DOM Elements?
There has to be a simpler way, I think. I already created a whole lotta files for such a simple behaviour that I'm thinking I'm totally on the wrong track.
My gut feeling is: That is view logic and the view should just observe a few states in the template and that's it.
What's the leanest approach to the problem?
I don't know if I understand your question right, but why you want to add the class open to the corresponding upper element? It automatically get active assigned. And with correct CSS it should work as expected.
I have created a small example demonstrating what I mean. Please have a look and let me know, if that's the solution for you or what's your problem with this solution.
http://emberjs.jsbin.com/wifusosadega/7/edit
EDIT
Here is a Bootstrap flavored version: http://emberjs.jsbin.com/wifusosadega/9/edit .
Routing in Ember.js is troubling me, and I can't seem to find the "correct" way of doing what I want to do.
I have a route, let's call it #/map, which contains a series of top-level and containers of child views.
Hierarchically, I have a top map_view, which contains 4 additional views: A topbar (which has topbar menu item triggers within it), a sidebar (which has sidebar menu item triggers in it), and two containerViews (a sidebar menu containerView and a topbar menu containerView), which will contain one or more nested views that are programatically inserted on clicking a menu item trigger.
My issue is that while this works, and I can embed all of these views into their various templates, none of them are linking with controllers, and the controller they are picking up is the map_controller (which makes sense as that is the linked outlet controller for the top level view). Currently I am using a method described on Ember's github here, but it seems a little...hacky?
Here is a JSFiddle showing the problem. Notice that the controller for level-one-entry and level-two-entry is the index_controller: http://jsfiddle.net/fishbowl/Z94ZY/3/
Here are some code snippets for what I am doing to get around it:
map.hbs:
<section id='map'>
{{view App.SidebarView}}
{{view App.TopbarView}}
<div id='map-canvas'></div>
</section>
topbar_view.js:
var TopbarView = Em.View.extend({
templateName: 'topbar',
classNames: ['topbar-container'],
init: function() {
var content = this.get('content'),
controller = App.TopbarController.create({
view: this
});
this.set('controller', controller);
this._super();
}
});
module.exports = TopbarView;
topbar_controller.js
var TopbarController = App.ApplicationController.extend({
content: Ember.computed.alias('view.content'),
trigger: null,
start_date: null,
end_date: null,
travelling: null,
word: 'topbar'
});
module.exports = TopbarController;
I'm not doing anything special in the router other than declaring this.route('map'). A further problem i'm having is that whenever I declare needs: ['some_other_controller'], I get an error
<App.TopbarController:ember413> specifies 'needs', but does not have a container. Please ensure this controller was instantiated with a container.
Am I missing something blindingly obvious about how to go about linking these together. I'm guessing that i'm using routing incorrectly. I don't want to change what the URL is, as i'm technically not moving pages, just opening and closing menus on the page, but I don't really understand how else i'm supposed to use the router to achieve this.
EDIT 2: i've mocked up another jsfiddle of what I could do with outlets and link-to's, but i'm not sure that I want the URL changing (as you'd probably be able to do odd things with the back button etc): jsfiddle - The alternative to this is to set location: 'none' in the Router, but I don't really like that option either...
I have a working solution in regard to rendering layouts with views in regions in a Marionette application I'm working on, but something doesn't feel right about it. Do you have to append anything directly to the DOM?
Here is my method in the controller:
//Grab the main Layout
var layout = new APP.Views.LayoutView();
//Render that layout
layout.render();
//Make the model
var simpleModel = new APP.Models.simpleModel({
"field1" : "foo",
"field2" : "bar"
});
layout.header.show(new APP.Views.subView({model: simpleModel}));
$('body').html(layout.el);
It's the last part that feels unnatural to me. This is primarily because if I move 'layout.header.show' to after the .html(), then it doesn't render properly. What's the point of regions if they aren't dynamically changable after pushing it to the DOM?
Here is my layout:
APP.Views.LayoutView = Backbone.Marionette.Layout.extend({
template: "#layoutTemplate",
className : 'wrapper',
regions: {
header: "#header",
footer: "#footer"
}
});
and here is the sub view:
APP.Views.subView = Backbone.Marionette.ItemView.extend({
template : '#headerTemplate',
className: 'container'
});
As I said, this works, but it feels like I'm not using regions properly. Is there a better, more concise way to do this that will allow you access to regions after you rend the layout to the DOM?
In the Marionette documentation there seems to be no mention of using .html() to get things on the page -- I'm wondering if this is left out because it's not needed or that it's assumed.
Can anyone help?
Okay, so it seems like this can be circumvented by creating a 'region' in your application, then using .show() to show the layouts inside of it.
Here is a link to a fiddle I found on SO that helped: http://jsfiddle.net/TDqkj/6/
as well as to another question: Understanding layouts in Marionette for Backbone.js
The fiddle in particular has this code:
App = new Backbone.Marionette.Application();
App.addRegions({
'nav': '#nav',
'content': '#content'
});
Which the programmer than uses to add layouts to those regions -- meaning you never have to append to the DOM.
If anyone else has a more elegant solution, please post!