I'm running into a problem getting Backbone.Marionette to cooperate with Twitter Bootstrap. Bootstrap expects markup to conform to predefined patterns in order to work, but the way that Backbone and Marionette handle things, it seems to be impossible to use a Marionette.Layout to generate Bootstrap-compliant markup for a navbar with an embedded dropdown.
Here's the problem:
Let's say I have a Marionette.Layout representing the navbar, and I want to have a region, where I will put the CollectionView or CompositeView that manages the items in the dropdown. The region is a selector, so in this case if we have:
<div class="navbar">
<ul class="nav">
<li>Static item</li>
<li id="dynamic-dropdown"></li>
</ul>
</div>
Then the Layout would specify the region for the dynamic dropdown as follows:
Marionette.Layout.extend({
regions: {
dropdown: '#dynamic-dropdown'
}
...
});
I would then have a CompositeView that takes care of rendering the dropdown item models, specifies the extra markup for the Bootstrap dropdown, and so forth. The problem is that it appears to be impossible to make #dynamic-dropdown be the $el for the CompositeView, as Backbone always inserts an extra div (or whatever you specify in tagName). In order for the dropdown to appear as expected, I need to get rid of that extra element and have the view's root be #dynamic-dropdown.
I've put together a jsfiddle illustrating what I mean.
tl;dr How do I make a View's root element be the region specified in a Marionette.Layout?
You just need to make sure that the markup that is generated in the end follows the same as required by Bootstrap (use the console to debug).
The required markup is as follows:
The markup generated by your code has the following problems:
So, to fix it you have to change your nav template to:
<script type="text/html" id="nav">
<div class="navbar">
<ul class="nav" id="dropdown">
</ul>
</div>
</script>
And your views to render <li> instead of <div>
var DropdownItem = Marionette.ItemView.extend({
tagName: 'li',
template: "#dropdown-item"
});
var DropdownView = Marionette.CompositeView.extend({
template: "#dropdown-collection",
className: 'dropdown',
tagName: 'li',
itemView: DropdownItem,
itemViewContainer: '#dropdown-items'
});
Here is the working version: http://jsfiddle.net/7auhR/6/
Related
A few months ago I've be created the topic: Try to render two templates in same View (Ui-Router), where I asked about how to render multiple views in the same page. My objective with this was created a web app as an desktop one, with views to minimize, maximize, close and stuff like that.
Well, my app is ready but I'm getting a problem, when I up my app to the production some computers are taking a long time to render all the Views. In the image bellow we can se a lot of requisitions that server take to return my templatesURL's.
There is a way to avoid this ? I was looking for an lazy load to templateURL but I Didn't find any. :(
This plunkr was the approach what I used. I have only one state for all my Views (My current app.config has 103 Views):
routerApp.config(function($stateProvider) {
$stateProvider.state('mainState', {
views: {
'CompanyView': {
templateUrl: 'Company.html'
},
'PeopleView': {
templateUrl: 'People.html'
},
.....
....
}
})
});
Introduction
The way you approached the solution is the cause of the problem you're facing, because you have too many views for a single state, it'll end up having to load all of them in order to set that state, so every time you access your state, ui-router has to load every template in order to set the views. It might not cause problem for a few number of templates, but, for larger numbers like yours it is definitely an issue.
Ng-Templates
You can try to cache your templates in your page using <script type="text/ng-template"... in order to prevent the loading time, it's a good practice by the way. Usually it's part of the production build optimization, load all templates in the template cache, so that the application load time decreases significantly provided that you don't have to wait for an http call to load a page. It will indeed increase the performance in your case, but I don't have a benchmark that ensure if it'd be enough for your scenario.
Component Based Solution
Anyhow, you can always implement interface components to behave the way you want, optimized in such a way that it doesn't have to load one hundred templates to show a single panel for the user.
My suggestion is, instead of using ui-router, use a component based solution, create a directive component to hold the panel content of each window and its behavior; and use a controller to manage the state of opened and closed panels, holding and managing each opened panel in a list and so on. For example:
<nav>
<button ng-click="openPanel({title: 'My Panel Title', templateUrl: 'myPanel.html'>">
Open myPanel
</button>
<nav>
<main>
<panel ng-repeat="panel in openedPanels"></panel>
</main>
The following snippet implements this approach using bootstrap 4 css, each panel is a bootstrap card, and it has a list of panels it can open and on click of a nav list it adds the respective panel to the opened panels list where angularjs can render it on the html using ng-repeat. This way, only the opened window will be rendered, therefore, only the opened window template will be loaded.
Disclaimer: This is a very simple example implemented not using the best practices available out there. If you intend to use this approach you should implement it based on your application to fit better the needs of your architecture, this one is not a complete functional component, it's just an example for the sake of the demonstration.
angular.module('app', [])
.controller('PanelsCtrl', function($scope) {
// available windows to be opened
$scope.panels = [
{ title: 'Window 1', templateUrl: 'window1.html' },
{ title: 'Window 2', templateUrl: 'window2.html' }];
// all currently opened panels
$scope.openedPanels = [];
// opens a panel (a.k.a, adds a panel
// to the opened panels list)
$scope.openPanel = function(panel) {
if ($scope.openedPanels.indexOf(panel) === -1)
$scope.openedPanels.push(panel);
};
// close a panel (a.k.a, removes a panel
// from the opened panels list)
$scope.onClosePanel = function(panel) {
$scope.openedPanels.splice($scope.openedPanels.indexOf(panel), 1);
};
})
.directive('window', function($templateRequest, $templateCache, $compile) {
return {
restrict: 'E',
scope: {
panel: '=',
onClosePanel: '&'
},
template: `
<div class="card">
<h4 class="card-header">
<span>{{ panel.title }}</span>
<button
ng-click="onClosePanel(panel)"
type="button"
class="close"
data-dismiss="modal"
aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</h4>
<div class="card-body">
<ng-include src="panel.templateUrl"></ng-include>
</div>
</div>
`
}
})
// example controlelr to be used with ng-controller
.controller('Window1Ctrl', function($scope) {
$scope.window1Prop = 'This is a property from Window1Ctrl'
})
#import 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css'
<div ng-app="app">
<div class="container" ng-controller="PanelsCtrl">
<div class="row">
<div class="col-sm-3">
<ul class="nav flex-column">
<li class="nav-item" ng-repeat="panel in panels">
<a class="nav-link active" href="#" ng-click="openPanel(panel)">
{{ panel.title }}
</a>
</li>
</ul>
</div>
<div class="col-sm-9">
<window ng-repeat="panel in openedPanels" panel="panel" on-close-panel="onClosePanel(panel)">
</window>
</div>
</div>
</div>
<!-- NG-TEMPLATES -->
<script type="text/ng-template" id="window1.html">
<div ng-controller="Window1Ctrl">
<b>{{panel.title}}</b>
<h5>window1Prop: {{ window1Prop }}</p>
</div>
</script>
<script type="text/ng-template" id="window2.html">
<em>{{panel.title}}</em>
</script>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.js"></script>
On my site I have a navbar component that I want to customize for each and every ng-view I end up loading. Currently I'm doing this as follows. I have a NavCtrl for the navbar itself and my ng-view directive sits outside the scope of this controller. And I use a navbar service to change/override functionality in the navbar e.g each of my views need to override the click handler for a save button my navbar. The NavbarService has hookes to set the save function. And in the NavCtrl the $scope.save = NavbarService.save
var app = angular.module('myApp', ['ngRoute', 'ngResource']);
app.config(function($routeProvider) {
$routeProvider.when('/world', {templateUrl : 'world.html', controller : 'WorldCtrl'}).otherwise({templateUrl : 'hello.html', controller : 'HelloCtrl'});
});
app.service('NavbarService', function() {
var message = 'Save button not clicked yet',
saveFunction = function() {
this.setMessage('Default save called');
};
this.save = function() {
_saveFunction();
};
this.setSaveFunction = function(funct) {
_saveFunction = funct;
};
this.setMessage = function(newMessage) {
message = newMessage;
};
this.getMessage = function() {
return message;
}
});
app.controller('NavCtrl', function($scope, $location, NavbarService) {
$scope.message = NavbarService.getMessage();
$scope.save = NavbarService.save;
$scope.world = function() {
$location.path('/world');
};
$scope.hello = function() {
$location.path('/hello');
};
$scope.$watch(NavbarService.getMessage, function(newValue) {
$scope.message = newValue;
});
});
app.controller('HelloCtrl', function($scope, NavbarService) {
$scope.init = function() {
NavbarService.setSaveFunction(function() {
NavbarService.setMessage('Save method called from the HelloCtrl');
});
};
});
app.controller('WorldCtrl', function($scope, NavbarService) {
$scope.init = function() {
NavbarService.setSaveFunction(function() {
NavbarService.setMessage('Save method called from the WorldCtrl');
});
};
});
<html lang="en">
<head>
<title>My App</title>
</head>
<body ng-app="myApp">
<nav ng-controller="NavCtrl">
<button ng-click="save()">Save</button>
<button ng-click="world()">Go to world</button>
<button ng-click="hello()">Go to hello</button>
<pre>{{message}}</pre>
</nav>
<div ng-view onload="init()"></div>
<script type="text/ng-template" id="hello.html">
<h2>Active view is Hello</h2>
</script>
<script type="text/ng-template" id="world.html">
<h2>Active view is World</h2>
</script>
<script src="https://code.jquery.com/jquery-2.1.4.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular-route.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular-resource.js"></script>
</body>
</html>
I'm wondering if I'm making this too complicated. I think the same thing can be achieved by just nesting the ng-view directive within the NavCtrl's scope. That way I can override $scope.save in each view controller.
But most the documentation states that services are the preferred way to share resources across controllers. Is one way better than the other? Why?
Please advise. Thanks.
However I will have to repeat the <button ng-click="save()">Save</button> element in every template if since all my views need that button.
This comment clears up a few things, and in that case I do not necessarily recommend the dynamic/declarative approach. Instead, this particular UX can be considered more of a static UX and I'd do something a little differently that still avoids any service or scope inheritance. I suggest using the following:
app level
Your app level nav can still borrow the other answer's suggestion, however let's disregard the dynamic/declarative content. So the nav looks like:
<div class="navbar navbar-submenu2">
<ul class="nav navbar-nav navbar-submenu2">
<!-- container for dynamic view-specific menu content -->
<li nx-view-menu-container class="nav navbar-nav">
</li>
<!-- static nav items -->
<li class="nav navbar-nav">
<a ng-click="saveRqst()">
<span class="current-del-name">Save</span>
</a>
</li>
The app-level controller would have this:
.controller 'appController', [
'$scope'
($scope)->
$scope.saveRqst = ->
$scope.$root.$broadcast 'saveRqst', <addt. data if applicable>
]
per view level controller
This makes a few assumptions:
only on of these types of views are ever present (meaning it's one view/controller per application state, otherwise you might have 2 controllers trying to process a save rqst)
your views reside as a child within your application template
your views adhere to the normal view controller paradigm (meaning they're not special directives with isolate scopes)
.controller 'someController', [
'$scope'
($scope)->
$scope.$on 'saveRqst`, (data)-> #do something for this view
]
Now I know this looks kinda like the scope inheritance bit, but it's not. You have to define your save logic per view controller logic anyway, there's no getting around that if I've understood what you've presented in the question and other answer's comment. The one nice bit about this event bus approach is it's easy enough to track the logic. If you notate your code, then another developer who's had any experience working with Java or Flex or whatever, will easily be able to get up to speed on implementing additional views w. save logic.
So I've presented 2 approaches to this. One is the more dynamic, declarative approach and this one is the more static, yet unique-per-view approach.
preface & some history
Let me propose something that utilizes neither a service, nor scope inheritance. Here is a similar issue I was trying to tackle. I have views that get loaded via ui.router's ui-view (we're basically talking application states here right?). I have my normal application nav & subnav, something like this...
['findings', 'tasks', 'reports', 'blah']
...that are common to all views. But I also have some specific UI per view that I want wired to that view's $scope/controller. In other words, I may have a dropdown specific to the current view, and a typeahead for another view. I elected to tackle this declaratively with a directive rather than trying to force some inheritance override paradigm.
directives
nxViewMenuContainer
First off I made a directive that was simply a container for housing the declared elements specific the views' nav elements.
.directive 'nxViewMenuContainer', [
()->
dir =
restrict: 'EAC'
link: ($scope, elem, attrs)->
if !elem.attr 'id'
elem.attr 'id', 'nx-view-menu-container'
$scope.$root.$on '$stateChangeSuccess', ->
elem.empty()
]
This is implemented like so:
<div class="navbar navbar-submenu2">
<ul class="nav navbar-nav navbar-submenu2">
<!-- container for dynamic view-specific menu content -->
<li nx-view-menu-container class="nav navbar-nav">
</li>
<!-- static nav items -->
<li class="nav navbar-nav" ng-repeat="nav in app.subnav">
<a><span class="current-del-name">{{nav.label}}</span></a>
</li>
nxViewMenu
This directive is declared on each view and serves as the view-specific elements container that gets moved to the directive above.
.directive 'nxViewMenu', [
()->
dir =
restrict: 'EAC'
link: ($scope, elem, attrs)->
elem.ready ->
name = attrs.nxViewMenu or '#nx-view-menu-container'
target = $(name)
if !target.length
return
target.empty()
target.append elem
]
And inside each view where I might want a dynamic menu to appear in else where (in this case, in the app-level's nav container) I declare it inside my view templates.
view 1
<div class="nx-view-menu">
<a class="btn btn-default">
<i class="fa fa-lg fa-bars nx-clickout-filter"></i>
<!--<label name="panel-title" style="color:#58585b; padding-left:5px;" class="nx-clickout-filter">Quick Links</label>-->
</a>
</div>
view 2
<div class="nx-view-menu">
<input typeahead="...">
</div>
view 3
<div class="nx-view-menu">
<date-picker>...</date-picker>
</div>
closing arguments
Firstly you are doing things declaratively so it's easy to follow the logic once you understand the directives.
It keeps the top level controllers ignorant of the specific views' logic while still displaying the DOM in a consistent way
It bypasses the need for special services or some hacky message bus
It simplifies your overall DOM structure because you don't need to set up some dual-view containers (in my case that was the other option in using ui.router) or parallel states.
I'm using Bootstrap and AngularJS to dynamically create clickable tabs in HTML. More specifically, I'm using the ng-switch and ng-switch-when directives to control which tab's content is displayed at any given time.
PLEASE NOTE: Below you'll notice I use a controller for DOM manipulation. I realize the "AngularJS way" to perform DOM manipulation is with directives (and not controllers). I'm not one to deliberately violate good coding practices, but since I'm trying to understand this exact issue I've stumbled across while learning about controllers and directives, I'm asking you to only consider using a controller for now since that's what I believe may be contributing to the issue.
If you keep reading you'll notice I've already found a directive-based solution (i.e., the "AngularJS way"), but I'm specifically asking about the issue that occurs when using a controller and ng-switch-when.
The Problem:
The issue I'm facing is as follows. When I append div tags (containing a ng-switch-when directive) onto a container, and then compile these tags using $compile, somehow the content of each tag using ng-switch-when becomes duplicated. To keep this question from becoming too long, I've placed the idea behind what the code's expected behavior is in the comments of the first JSFiddle below, which demonstrates the issue.
JSFiddle: What I have now -- Uses controller for DOM manipulation ("non-AngularJS way"), results in unexpected duplication of tags using ng-switch-when.
An in-depth description of how to reproduce the duplication is as follows. Upon creating a new tab, it appears that the previously clicked tab's content (within the div containing ng-switch-when) is duplicated. You can see this happen by loading the above JSFiddle, right-clicking the Default text, selecting Inspect element, and then pressing Analysis > Create Tab in the tabbed pane. You'll see there are now two div tags with ng-switch-when="Default" instead of just one. If you continue to create new tabs, this pattern continues for view1, view2, and so on. Alternatively, just click Analysis > Create Tab 5 times and then look at the duplicates in Tabs 1-4. Either way, you'll see the previously selected tab's contents are doubled whenever a new tab is created.
Frustrated by this unexpected behavior, I went ahead and implemented a directive-based solution using AngularUI's Bootstrap components, which creates custom directives for tab components, to gain the desired functionality.
JSFiddle: What I want to happen in the above JSFiddle -- Uses directives for DOM manipulation ("AngularJS way").
However, as I've mentioned, I want to know why my first JSFiddle is not working. See below for formal question.
Points to consider:
The issue may be related to scope, the order the scripts are loaded, or possibly a logic error.
Perhaps it's something AngularJS simply cannot do. I'm aware that compound transclusion no longer works as of v1.2, and this issue may be related.
So my question is as follows:
Why is this duplication occurring? Is there a way to prevent/workaround the duplicates from being created? Remember, I wouldn't use this in practice, I'm just curious if this issue can be solved at all.
PLEASE NOTE: I am not looking to resolve this using jQuery, such as this jQuery solution. Although I'm aware AngularJS uses a lighter version of jQuery called jqLite under the hood, I'm looking for a "pure AngularJS" solution.
HTML:
<!-- Include CDNs (see JSFiddle above) -->
<!-- Wrap in HTML tags, include DOCTYPE -->
<body ng-app="myApp">
<div class="panel panel-default" ng-controller="myCtrl">
<div class="panel-heading">
<ul class="nav nav-tabs">
<li class="active">
<a class="h4">My App</a>
</li>
<li class="dropdown">
<a href="" class="dropdown-toggle h4" data-toggle="dropdown">
Analysis
<span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li>
<button id="createTabLink" class="h4" ng-click="createTabClick()">Create Tab</button>
</li>
</ul>
</li>
</ul>
</div>
<div class="panel-body">
<div class="panel panel-default">
<div class="panel-heading">
<ul id="createTabDiv" class="nav nav-tabs"></ul>
</div>
<div class="panel-body">
<div id="viewWrapper" ng-switch="" on="viewSelected">
<div id="Default" ng-switch-when="Default">Default</div>
</div>
</div>
</div>
</div>
</div>
</body>
JS:
angular.module('myApp', []);
function myCtrl($scope, $compile) {
$scope.count = 0;
$scope.viewSelected = "Default";
$scope.tabClick = function($event) {
var tab = $event.target;
var viewName = tab.getAttribute("value");
$scope.viewSelected = viewName;
};
$scope.createTabClick = function() {
$scope.count++;
var viewName = "view" + $scope.count;
var createTabDiv = angular.element(document.querySelector('#createTabDiv'));
var viewWrapper = angular.element(document.querySelector('#viewWrapper'));
var $comp1 = createTabDiv.append('<li class="active"><a class="h4" value="' + viewName + '" ng-click="tabClick($event)">Tab ' + $scope.count + '</a></li>');
$compile($comp1)($scope);
var $comp2 = viewWrapper.append('<div id="' + viewName + '" ng-switch-when="' + viewName + '">' + viewName + '</div>');
$compile($comp2)($scope);
$scope.viewSelected = viewName;
};
}
The question may confuse you, I want to achieve the 'tab' widget from 'ul' and 'li' tags using 'Backbone.js' and it's MVC concept. How to do?
For example
<ul>
<li> One </li>
<li> Two </li>
<li> Three </li>
</ul>
When the link 'one' is clicked then I want to show some content (div) and want to hide others how to do using MVC concept ?
This could be done without Backbone and MVC, but I want to do this with Backbone, please help me.
You can create a Backbone.View that will handle all of your "tabs". This Backbone.View should cover the or whatever is above your content.
Based on your ul, you can add CSS selectors or other attributes:
<div id="mainDiv">
<ul>
<li class="tab-one">One</li>
<li class="tab-two">Two</li>
<li class="tab-three">Threeli>
</ul>
<div id="tab1" class="tab">...</div>
<div id="tab2" class="tab">...</div>
<div id="tab3" class="tab">...</div>
</div>
Create a Backbone.View to handle the page events:
MyView = Backbone.View.extend({
el: $('#mainDiv'),
events: {
'click .tab-one': 'showTabOne',
'click .tab-two': 'showTabTwo',
'click .tab-three': 'showTabThree'
},
showTabOne: function() {
$(this.el).find('.tab').hide();
$(this.el).find('#tab1').show();
},
showTabTwo: function() {
$(this.el).find('.tab').hide();
$(this.el).find('#tab2').show();
},
showTabThree: function() {
$(this.el).find('.tab').hide();
$(this.el).find('#tab3').show();
}
...
}
This is just to show and hide tabs using Backbone. You can do a lot more with Backbone :)
Edit: If you're tab list is dynamic, you can put the initialization of events using jquery way inside the Backbone.View's initialize() function. This will get invoked once you instantiate a Backbone.View (var view = new MyView;)
I am trying out Backbone.Marionette and I am confused as to why my Layouts and ItemViews keep generating extra divs.
example is in Coffee btw.
AppLayout = Backbone.Marionette.Layout.extend
template: "#my-layout",
regions:
menu: "#menu",
content: "#content"
MyMenuView = Backbone.Marionette.ItemView.extend
template: '#project_wiz_nav_template'
MyContentView = Backbone.Marionette.ItemView.extend
template: '#project_setup_template'
MyApp = new Backbone.Marionette.Application()
MyApp.addRegions
mainRegion: '#project'
MyApp.addInitializer ->
layout = new AppLayout()
MyApp.mainRegion.show(layout)
layout.menu.show(new MyMenuView())
layout.content.show(new MyContentView())
MyApp.start()
This is what index.html contains:
<div id='project'></div>
<script type='text/template' id='project_wiz_nav_template'> <h2>HI</h2> </script>
<script type='text/template' id='project_setup_template'> <h2>WORLD</h2> </script>
<script id="my-layout" type="text/template">
<h2>Hello!</h2>
<div id="menu"></div>
<div id="content"></div>
</script>
This is what it produces:
<div id="project">
<div>
<h2>Hello!</h2>
<div id="menu">
<div>
<h2>HI</h2>
</div>
</div>
<div id="content">
<div>
<h2>WORLD</h2>
</div>
</div>
</div>
</div>
As you can see, it keeps generating extra divs for the views and the layouts. I've tried adding el: '#menu' and el: '#content' to no avail.
This is not because of Marionette. Backbone generates a <div> class for you by default. You can set the tag via the tagName attribute. See comments on the question for duplicates of this.
A hacky workaround, but jQuery's closest() actually did the job for me. Rather than using the returned myView.el directly, I'm using $(myView.el).closest("div").html() -- as I said, hacky, but as a short-term fix it's working.
I was tinkering with this tutorial: http://davidsulc.com/blog/2013/02/03/tutorial-nested-views-using-backbone-marionettes-compositeview/comment-page-1/#comment-3801, which takes a nested model and creates an accordion view using Bootstrap. I wanted to do the same with his starting point, only using the jQueryUI accordion widget, which is the reason I needed an unwrapped view coming back -- hence the filtering with closest().
Other than adding the jqueryUI links and changing the returned HTML as delineated above, it's working pretty well: http://dartsleague.parentleafarm.com/superheroes/
Specify your el property. I think that will fix it:
http://documentcloud.github.com/backbone/#View-el