AngularJS - What is the right way for this? - angularjs

I created a page, in which you can view lots of details on a user, in panels.
Every panel contains some information, received from a service, and sometimes has actions on it.
I have about 10 panels, and the viewer decides what he wants to see.
Is it right to make each of those panels a separate controller,
OR only panels with actions need to be in a separate controller, OR all the panels in one controller?
If every panel is a controller, and my code is this:
div class="panel panel-default">
<div class="panel-heading">Panel Heading</div>
<div class="panel-body" ng-controller="historyPanel" ng-include="'panels/history.html'"></div>
</div>
Is there a way for me to know if a panel is empty?
For example, I have an history panel, that I wanna show only when there is history to show, and I don't wanna show "No History", just wanna hide that panel.
But I do want to keep the panel code outside of the history.html view.

You should consider creating a directive for this. You can either create a single directive that contains all 10 panels, and provide an attribute to specify which panel to show, or you could create 10 different directives.
Something like:
<my-panel panel="history" ng-show="userPanel.history.display"></my-panel>
<my-panel panel="settings" ng-show="userPanel.settings.display"></my-panel>
<my-panel panel="friends" ng-show="userPanel.friends.display"></my-panel>
<my-panel panel="music" ng-show="userPanel.music.display"></my-panel>
Then your scope controller might have something like:
app.controller('MyController', function($scope) {
$scope.userPanel = {
history: { display: true },
settings: { display: true },
friends: { display: true },
music: { display: false }
}
});
This way, you could load data from a service which has the user preferences for which panels are displayed.

Try something like this, try not to inject $scope at all cost. :)
function PanelCtrl() {
'use strict';
var ctrl = this;
ctrl.panels = [{
"id": 0,
"name": "Lucia Oconnor",
"history": "my history"
}, {
"id": 1,
"name": "Stevenson Mcintosh",
"history": "my history"
}, {
"id": 2,
"name": "Spears Sims",
"history": "my history"
}];
}
function panel() {
'use strict';
return {
restrict: 'E',
scope: {},
bindToController: {
//Depends what youd like to do with the PanelCtrl
//'&': use a function on that ctrl
//'=': two way data binding
},
controller: 'PanelCtrl as ctrl',
templateUrl: './templates/panel.ng.html'
};
}
angular
.module('app', [])
.controller('PanelCtrl', PanelCtrl)
.directive('panel', panel);
This would be that template:
//also if static content might want to use--one time bindings.
// that would be ::panel.name
<div class="panel panel-default" id="panel_{{panel.id}}">
<div class="panel-heading">{{Panel.name}}</div>
<div class="panel-body">{{Panel.history}}</div>
</div>
This would be your html:
//be sure to include track by, major perf gains.
<div class="container" ng-repeat="panel in ctrl.panels track by panel.id">
<panel bind="here" ng-if="ctrl.history.length"></panel>
</div>

Related

How to Redraw Tables using Angular UI Tabs and UI Grid?

I'm trying to migrate from jQuery Data Tables to Angular UI. My app uses the MySQL World schema and displays the data in 3 different tables. In jQuery, I'd 3 different pages, each launched from a home page. Here's a live example.
In Angular, I created 3 tabs, clicking on each of which is supposed to reload the page with a new grid and populate it with data. Problem is, the grid gets displayed alright on page load with the content on the first tab. On clicking the other tabs, page goes empty and nothing is rendered. Now the data returned is not insignificant, sometimes around 4k rows. However, the problem isn't a latency issue as I've confirmed by waiting several minutes.
I'm not a JS/CSS/HTML guy so most likely I'm missing something. That's why this question.
Edit:
Plnkr
Following is the code:
HTML:
<body>
<div id="selection-panel" class="selection-panel" ng-controller="HelloWorldCtrl">
<div>
<uib-tabset type="pills" justified="true">
<uib-tab ng-repeat="tab in tabs" heading="{{tab.title}}" select="update(tab.title)">
<div id="data-panel" class="data-panel" ui-grid="gridOptions"></div>
</uib-tab>
</uib-tabset>
</div>
</div>
<script src="js/app.js"></script>
</body>
JS:
(function() {
var app = angular.module('helloWorld', ['ui.bootstrap', 'ui.grid']);
app.controller('HelloWorldCtrl', ['$http', '$scope', function ($http, $scope) {
$scope.tabs = [
{ title: 'Countries' },
{ title: 'Cities' },
{ title: 'Languages' }
];
$scope.gridOptions = {};
$scope.gridOptions.data = [];
$scope.gridOptionsCountries = {
columnDefs: [
{ name: 'code'},
{ name: 'name'},
{ name: 'continent'},
{ name: 'region'},
{ name: 'population'}
]
};
$scope.gridOptionsCities = {
columnDefs: [
{ name: 'id'},
{ name: 'name'},
{ name: 'country'},
{ name: 'population'}
]
};
$scope.gridOptionsLanguages = {
columnDefs: [
{ name: 'country'},
{ name: 'language'}
]
};
$scope.update = function(title) {
if (title === "Countries") {
$scope.gridOptions = angular.copy($scope.gridOptionsCountries);
} else if (title === "Cities") {
$scope.gridOptions = angular.copy($scope.gridOptionsCities);
} else if (title === "Languages") {
$scope.gridOptions = angular.copy($scope.gridOptionsLanguages);
}
$http.get(title.toLowerCase()).success(function(data) {
$scope.gridOptions.data = data;
});
};
}]);
})();
I see 2 problems here:
You are creating/changing gridOptions dinamically. This is not the usual way of doing things and can bring many problems.
You are using grids inside of uib-tabs and this, like uib-modals, can have some annoying side effects.
I'd suggest you to address the first issue using different gridOptions (as you do when you create them) and then putting them inside your tabs array children, this way you can refer them from the html this whay:
<uib-tab ng-repeat="tab in tabs" heading="{{tab.title}}" select="update(tab.title)">
<div id="data-panel" class="data-panel" ui-grid="tab.gridOptions"></div>
</uib-tab>
The second problem is quite known and inside this tutorial they explain how to address it: you should add a $interval instruction to refresh the grid for some time after it's updated in order to let the tab take its time to load and render.
The code should be as follows:
$scope.tabs[0].gridOptions.data = data;
$interval( function() {
$scope.gridCountriesApi.core.handleWindowResize();
}, 10, 500);
Where gridCountriesApi are created inside of a regular onRegisterApi method.
I edited your plunkr, so you can see the whole code.
I can not get this to work in tabs, my guess is because you first use ng-repeat which creates a scope for each iteration, and then maybe the tabs itself creates a new scope and this causes a lot of headache with updates.
The quickest solution is just to move the grid outside of the tabs.
Here is the updated html.
HTML
<body>
<div id="selection-panel" class="selection-panel" ng-controller="HelloWorldCtrl">
<div>
<uib-tabset type="pills" justified="true">
<uib-tab ng-repeat="tab in tabs" heading="{{tab.title}}" select="update(tab.title)"></div>
</uib-tab>
</uib-tabset>
<!-- This is moved outside -->
<div id="data-panel" class="data-panel" ui-grid="gridOptions">
</div>
</div>
<script src="js/app.js"></script>
</body>
In my situation tab contents consume important time to be loaded. So if your case is that you don't want to update tab content everytime the tab is clicked, you can use this workaround:
In the HTML part I use select property to indicate which tab is pressed:
<tabset justified="true">
<tab heading="Tab 1" select="setFlag('showTab1')">
<div ng-if="showTab1">
...
</div>
</tab>
</tabset>
In the tab (*) container I used a switch to recognize which tab is pressed and broadcast the press action:
case 'showTab1':
$scope.$broadcast('tab1Selected');
break;
And in the controller part I listen the event and handle resizing:
// The timeout 0 function ensures that the code is run zero milliseconds after angular has finished processing the document.
$scope.$on('tab1Selected', function () {
$timeout(function() {
$scope.gridApi1.core.handleWindowResize();
}, 0);
});
Here is my Plunkr. Hope it helps.
(*) For current bootstrap version you should use and

Wrap ag-grid in an Angular Directive

I am creating a wizard to add a new appointment in our application. The last page of the wizard contains a tabbed section with all potential conflicts based on several criteria. Each tab is one of the criteria and uses an Angular Grid to show the list of conflicts. Since each grid has the same columns, but contains different data, I would like to use a directive to wrap the Angular Grid and its grid options in the Template and then set the rowData in another attribute on my directive. I currently have the following for my directive:
'use strict';
app.directive('inApptConflict', ['angularGrid', function (angularGrid) {
return {
restrict: 'A',
transclude: true,
require: '?ngModel',
template: '<div class="ag-fresh conflictGrid" ag-grid="{{ conflictGridOptions }} ng-transclude"></div>',
controller: function ($scope) {
// function for displaying dates in grid
function datetimeCellRendererFunc(params) {...}
// column definitions
var conflictColumnDefs = [
{ colId: "Id", field: "Id", hide: true },
{ colId: "StartTime", field: "StartTime", headerName: "Start", width: 150, cellRenderer: datetimeCellRendererFunc } ...
];
// Grid options
$scope.conflictGridOptions = {
columnDefs: conflictColumnDefs,
rowData: null,
angularCompileRows: true,
enableColReseize: true
};
},
link: function ($scope, $elem, $attrs, ngModel) {
$scope.conflictGridOptions.rowData = ngModel;
$scope.conflictGridOptions.api.onNewRows();
}
};
}]);
My view has the following code:
<!-- Tab panes -->
<div role="tabpanel" class="tab-pane fade in active" id="conflicts1" data-ng-show="apptCtrl.conflicts1">
<div in-appt-conflict data-ng-model="apptCtrl.conflicts1"></div>
</div>
<div role="tabpanel" class="tab-pane fade" id="conflicts2" data-ng-show="apptCtrl.conflicts2">
<div in-appt-conflict data-ng-model="apptCtrl.conflicts2"></div>
</div>
Whenever I run this, I end up with the following error:
Error: [$injector:unpr] Unknown provider: angularGridProvider <- angularGrid <- inApptConflictDirective
I am not sure what else I need to do to get the directive to recognize ag-grid. I have tried using $compile, as well, but end up with the same error.
Is there something else that needs to be added to call a third party module from a directive? This did work when I used the grid three separate times with three separate grid options.
Thanks in advance for any help!
There is no need to inject 'angularGrid' in your directive (and there is no such injectable element).
All registered directives are available to all templates as soon as you register them in the angular module.
The only thing you need is to add 'agGrid' to the dependency of your angular module with something like
var module = angular.module("example", ["agGrid"]); then you case use ag-grid in your templates and directives.
See ag-grid documentation for more details.
So remove 'angularGrid' from line app.directive('inApptConflict', ['angularGrid', function (angularGrid) { and you should be good to go.

AngularJS: Sharing scope between multiple instances of same directive

I'm writing an app that uses the same table with the same data in multiple places. I created a custom directive that allows me to reuse this table. Unfortunately, if I edit the table in one instance, the other instance does not refresh. How do I link these two so that any edits I make to one show up in the other?
It sounds like you've mostly figured it out, the hard part is getting your data into a shape where the videos and photos can be shared by the slide show. I recommend doing this in a shared data access object returned by a separate factory in Angular, rather than directly in a scope. I've got a sample in Plunkr if it helps.
The sample has a directives that binds to shared data, retrieved from a factory as an object injected into two separate scopes. In your case, you would have to add methods to retrieve data from the server, and shape it for display.
testApp.factory("News", [function () {
var news = {
"stories": [
{"date": new Date("2015-03-01"), "title": "Stuff happened"},
{"date": new Date("2015-02-28"), "title": "Bad weather coming"},
{"date": new Date("2015-02-27"), "title": "Dog bites man"}
],
"addStory": function (title) {
var story = {
"date": new Date(),
"title": title
};
news.stories.push(story);
}
};
return news;
}]);
Both controllers reference the same factory for the data:
testApp.controller("FirstController",
["$scope", "News", function ($scope, news) {
$scope.news = news;
}]);
testApp.controller("SecondController",
["$scope", "News", function ($scope, news) {
$scope.news = news;
}]);
Views then pass the data into to the news list directive, which both shares the data and keeps the directive relatively dumb.
<div ng-controller="FirstController">
<news-list news="news" title="'First List'"></news-list>
</div>
<div ng-controller="SecondController">
<news-list news="news" title="'Second List'"></news-list>
</div>
The news-list directive is just dumb formatting in this example:
testApp.directive("newsList",
function() {
var directive = {
"restrict": "E",
"replace": false,
"templateUrl": "news-list.html",
"scope": {
"news": "=news",
"title": "=title"
}
};
return directive;
});
View template:
<div class="news-list">
<p>{{title}}</p>
<ul>
<li ng-repeat="story in news.stories | orderBy:'date':true">{{story.date | date:'short'}}: {{story.title}}</li>
</ul>
<form>
<input type="text" id="newTitle" ng-model="newTitle" />
<button ng-click="news.addStory(newTitle)">Add</button>
</form>
</div>

Menu and different submenu on click

I am trying to build a menu and a submenu in angular.
What I want to do is to have two arrays of objects
Menu
menu = [{name: 'Name1', link: '/link1'}, {name: 'Name2', link: '/link2'}]
submenu = [[{name: 'SubName1', link: '/Sublink1'}, {name: 'SubName1', link: '/sublink1'}],
[[{name: 'SubName2', link: '/Sublink2'}, {name: 'SubName2', link: '/sublink2'}]]
So when I click Name1 the first array of SubMenu will be selected and when clicking Name2 the second array will be selected.
How I can create two Directives one for the main menu and one for the second and be able to communicate between them on click. I have tried building this in a controller, I was able to select the submenu by using the $index, but the submenu can't be moved around as I like because it needs to be under the controller.
I finally managed to solve my problem here is the solution: http://jsfiddle.net/4kjjyL4s/4/
How can I improve my solution?
Don't reinvent the wheel :) UI router is a prepackaged solution that handles nested routing for you.
If you have a menu of items and you want to display another menu of items when one of the items is selected UI router does exactly that. https://github.com/angular-ui/ui-router
Can't give you the exact answer because information is lacking, but for example if you're using the directives with different menu items at other places in your app, I'd recommend to pass the menu array from controller (ng-controller, not directive's controller) through directive's scope.
Also, you're looking for kinda standard way for directives to communicate directly (in your case, communication between menu and submenu directive to notify the item selection change), use directive's controller. Here's a good tutorial.
https://thinkster.io/egghead/directive-to-directive-communication/
To communicate between controllers or directives, you should use services.
From the angular guide ( https://docs.angularjs.org/guide/services ):
Angular services are substitutable objects that are wired together using dependency injection (DI). You can use services to organize and share code across your app.
I checked the code you posted on jsfiddle ( http://jsfiddle.net/4kjjyL4s/4/ )and I tried to keep the most of it. Below are my changes in the JavaScript file ( please, read the comments in the code ).
var app = angular.module("app",[]);
app.controller('main', function(){});
// The service will be responsible for the shared objects and logic
app.service('MenuService', function () {
var list = [
{
name: "Menu1", link: "#menu1",
submenu: [
{ name: "Menu1Sub1", link: "#submenu1" },
{ name: "Menu1Sub2", link: "#submenu2" }
]
},
{
name: "Menu2", link: "#menu2",
submenu: [
{ name: "Menu2Sub1", link: "#submenu1" },
{ name: "Menu2Sub2", link: "#submenu2" }
]
}
];
var selected = [];
// methods and attributes published under the **this**
// keyword will be publicly available
this.getMenu = function () {
return list;
};
this.getSubmenu = function () {
return selected;
};
this.select = function ( menuItem ) {
// this does the *trick*!
// if we use the assignment operator here, we would replace the
// reference returned in the getSubmenu() method, so as the original
// reference did not change, angular's dirty checking would not detect it.
// using angular.copy() method, we are copying the contents of the
// selected submenu over the same reference returned by getSubmenu()
angular.copy( menuItem.submenu, selected );
};
});
// No $rootScope injection results in better re-usability. When you were
// relying in $rootScope sharing, both directives should live in the
// $rootScope, so if you add them inside a ng-controller created scope
// they would not work anymore
app.directive("menu", function() {
return {
restrict: "E",
// no need to isolate scope here, *scope:true* creates a new scope
// which inherits from the current scope
scope: true,
// with controllerAs (introduced in angular 1.2), you can skip
// polluting the scope injection.
controllerAs: "ctrl",
controller: function( MenuService ) {
this.list = MenuService.getMenu();
this.changeSub = function ( menuItem ) { MenuService.select( menuItem ); };
},
template: "<div ng-repeat='menu in ctrl.list'><button ng-click='ctrl.changeSub(menu)'>{{menu.name}}</button></div>"
};
});
app.directive("submenu", function() {
return {
restrict: "E",
scope: true,
controllerAs: "ctrl",
controller: function( MenuService ) {
this.sublist = MenuService.getSubmenu();
},
template: "<span ng-repeat='menu in ctrl.sublist'>{{menu.name}} | </span>aa"
};
});
And here is the updated HTML file, just to show both directives work now not directly inserted in the $rootScope
<div ng-app="app">
<div ng-controller="main">
<menu></menu>
<h1>Hello World!</h1>
<div class="main-content">
<submenu></submenu>
</div>
</div>
</div>
Hope it helps!
Try this Code:
function MyCtrl ($scope) {
$scope.subMenu = []; // default is false
$scope.toggleSubMenu = function (index) {
$scope.subMenu[index] = !$scope.subMenu[index];
};
}
HTML
<ul>
<li ng-class="{active: subMenu[0]}"> Name1
<ul>
<li>test</li>
<li>test</li>
<li>test</li>
</ul>
</li>
<li ng-class="{active: subMenu[1]}"> Name2
<ul>
<li>bar</li>
<li>bar</li>
<li>bar</li>
</ul>
</li>
</ul>
Also Check this

AngularJS: What is the best way to bind a directive value to a service value changed via a controller?

I want to create a "Header" service to handle the title, buttons, and color of it.
The main idea is to be able to customize this header with a single line in my controllers like this:
function HomeCtrl($scope, Header) {
Header.config('Header title', 'red', {'left': 'backBtn', 'right': 'menuBtn'});
}
So I created a service (for now I'm only focussing on the title):
app.service('Header', function() {
this.config = function(title, color, buttons) {
this.title = title;
}
});
...And a directive:
app.directive('header', ['Header', function(Header) {
return {
restrict: 'E',
replace: true,
template: '<div class="header">{{title}}</div>',
controller: function($scope, $element, $attrs) {
$scope.$watch(function() { return Header.title }, function() {
$scope.title = Header.title;
});
}
};
}]);
So, this actually works but I'm wondering if there are no better way to do it.
Especially the $watch on the Header.title property. Doesn't seem really clean to me.
Any idea on how to optimize this ?
Edit: My header is not in my view. So I can't directly change the $scope value from my controller.
Edit2: Here is some of my markup
<div class="app-container">
<header></header>
<div class="content" ng-view></div>
<footer></footer>
</div>
(Not sure this piece of html will help but I don't know which part would actually...)
Thanks.
If you are using title in your view, why use scope to hold the object, rather than the service? This way you would not need a directive to update scope.header, as the binding would update it if this object changes
function HomeCtrl($scope, Header) {
$scope.header = Header.config('Header title', 'red', {'left': 'backBtn', 'right': 'menuBtn'});
}
and refer to title as
<h1>{{header.title}}</h1>
Update
Put this in a controller that encapsulates the tags to bind to the header:
$scope.$on("$routeChangeSuccess", function($currentRoute, $previousRoute) {
//assume you can set this based on your $routeParams
$scope.header = Header.config($routeParams);
});
Simple solution may be to just add to rootScope. I always do this with a few truly global variables that every controller will need, mainly user login data etc.
app.run(function($rootScope){
$rootScope.appData={
"header" : {"title" : "foo"},
"user" :{}
};
});
.. then inject $rootScope into your controllers as warranted.

Resources