I am building my first Angular SPA. In a form I have three tabs, and want a tab to show only its contents and only when selected. The tab contents are in an array in the controller.
I have been playing around with different $scope configurations all evening, but can only make nothing, or the content of all 3 tabs appear at once. I can't isolate the content of one tab from the array in the view. I feel like I must be misunderstanding the $scope, but can't seem to find any documentation that fits the situation. Perhaps its completely the wrong way to go about it. In short, I would greatly appreciate any help or advice from you angular gurus out there :)
Thanks so much!! Here's the code:
I am keeping the tab content and functions as an array in the controller like this:
$scope.tabs = [
{
title: 'Free',
description: 'free',
price: 'free',
elem: 'free'
},{
title: 'Basic',
description: 'basic',
price: '9.97',
elem: 'basic'
},{
title: 'Unlimited',
description: 'Every service',
price: '19',
elem: 'unlimited'
}
];
$scope.currentTab = 'free';
$scope.onClickTab = function (tab) {
$scope.currentTab = tab.elem;
$('.tab-content').hide();
$('#'+tab.url).show();
}
$scope.isActiveTab = function(tabUrl) {
return tabUrl == $scope.currentTab;
}
I use ng-repeat to create the selection buttons:
<li class="tab-option" ng-repeat="tab in tabs"
ng-class="{active:isActiveTab(tab.elem)}"
ng-click="onClickTab(tab)">{{tab.elem}}
</li>
Below this, I want to display the price etc.. of the tab selected in a separate box. I see that {{tab.elem}} selects the correct part of the array, so I thought that {{tab.price}} would do the same.
But, when I use {{tab.price}} or any other {{tab.}} , like this:
<div>
<div class="spacer"></div>
<p>{{tab.description}}</p>
<p>{{tab.price}}</p>
</div>
I get nothing.
Thanks again for any help, advice or thoughts!
-Berkeley
The same functionality can be achieved in a different way
by the way we define onClickTab function.
Instead of storing $scope.currentTab as 'free' make it a reference to current tab
viz. $scope.currentTab=tab;
And then you can simply avoid the jquery selectors inside the function as it is not a good practise to do so.
In the content div where you need to show the price description use currentTab instead of tab.
Summarizing
<li class="tab-option" ng-repeat="tab in tabs"
ng-class="{active:isActiveTab(tab)}"
ng-click="onClickTab(tab)">{{tab.elem}}
</li>
Controller logic
//default one
$scope.currentTab=$scope.tabs[0];
$scope.isActiveTab = function(tabUrl) {
return tabUrl == $scope.currentTab;
}
$scope.onClickTab = function (tab) {
$scope.currentTab = tab.elem;
}
Content to be displayed by current tab
<div>
<div class="spacer"></div>
<p>{{currentTab.description}}</p>
<p>{{currentTab.price}}</p>
</div>
Always avoid hiding or showing elements in controller, as far as dom manipulations
are concerned make them using a directive.
Hope it helps.
I got it working!
The controller:
$scope.currentTab = $scope.tabs[0];
$scope.onClickTab = function (tab) {
$scope.currentTab = tab;
}
the view:
<div class="description">
<p class="text-center">{{currentTab.description}}</p>
<p class="text-center">{{currentTab.price}}</p>
</div>
This is all the code I needed. I used Naveen's advice of using the tab[order] to call it initially, but was struggling still with getting the comments to show up with the correct information only when selected.
I had been using $scope.currentTab = tab.elem; this doesn't work because it is trying to be too specific. By changing it to simply "tab" angular knew to treat all the tab's contents as the new $scope.currentTab so that when I call {{currentTab.description}} I get the correct response.
And with that I will close this question. Thanks so much!
Related
I’m hoping there are some Angular 1.x experts who can show me what I’m doing wrong. I have a simple function to update which of 3 buttons in a “tab group” is the current one. This function is called whenever any of the buttons is clicked.
$scope.updateFilter = function (type, value) {
// Additional unrelated code here ...
document.getElementsByClassName('active')[0].className = document.getElementsByClassName('active')[0].className.replace(' active', '');
document.getElementById('tabButton_' + value).className += ' active';
$scope.$apply();
};
The background color of the current button is indeed highlighted but only AFTER one clicks elsewhere on the screen. In other words, it’s not updated instantly like it should.
Any ideas how to correct this?
It's hard to diagnose the issue without seeing some more code or a reproduction of your existing issue. However, from the above, you are certainly not doing the "angularjs" way. A more angular approach would be to use bindings and update the model as the user clicks different button options. A very basic (and ugly styled) example:
angular.module('myApp', [])
.controller('MainController', function () {
var self = this;
self.$onInit = function $onInit() {
// These will be ng-repeated over for the example
self.buttons = [
'Option 1',
'Option 2',
'Option 3'
];
// This is the model binding that will drive the active style
self.activeIndex = 0;
};
self.setActiveIndex = function setActiveIndex(index) {
// This is called on button click and updates the model used
// for the active button styling
self.activeIndex = index;
};
});
.active {
background: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<!-- repeat the buttons. when clicked, call controller method to update model's active index -->
<div ng-app="myApp" ng-controller="MainController as $ctrl">
<button ng-repeat="b in $ctrl.buttons" type="button" ng-class="{active: $ctrl.activeIndex===$index}" ng-click="$ctrl.setActiveIndex($index)">{{::b}}</button>
</div>
Take aways:
You probably shouldn't be doing DOM manipulation. Use existing directives and model binding, else you are losing many of the benefits you are supposed to get from angularjs.
Don't call $scope.$apply(). Angular will do this for you if you are using an ng-click in your template (which you probably should instead of building the event listeners yourself).
I am trying to make a simple list display, but I am having some concerns about Controllers organization.
In my application, I have 2 states, items and state2. In items, I want to display a list of Items, and "something else" in state2.
But I also have a + button at the top of my application that can add an item to my list. And I want that button to be displayed in both states. Here is an illustration:
Now, I would like to put my items related functions, in a specific controller ItemsCtrl. So this would be my routes:
myApp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("/items");
$stateProvider
.state('items', {
url: "/items",
templateUrl: "partials/items.html",
controller: "ItemsCtrl"
})
.state('state2', {
url: "/state2",
templateUrl: "partials/state2.html",
controller: "State2Ctrl"
});
});
And this would be my ItemsCtrl:
myApp.controller('ItemsCtrl',function($scope){
$scope.items = ["One",'Two'];
})
And now, I make a new MainCtrl to handle the + button that should be present on any page:
myApp.controller('MainCtrl',function($scope){
$scope.promptItem = function(){
var result = prompt('Add Item', 'New Item', ['ok'], 'Zero');
$scope.items.push(result.input1); //This line doesn't work
}
})
What is the best organization for this kind of interface? Do I really need to put my $scope.items in my MainCtrl?
I'd rather not, and the best thing I think would even be to put the promptItem function in ItemsCtrl, what do you think?
Thanks a lot for your answers, I am completely new to this world :)
EDIT: Here is my HTML structure, my + button is in the root file:
<button ng-click="promptProduct()">Add Item</button>
<a ui-sref="state1">State 1</a>
<a ui-sref="state2">State 2</a>
<div ui-view></div>
This is probably a great place to leverage an Angular service. The problem from what I understand is that you are struggling to share state between different parts of your application. I would recommend creating, e.g. a ListService, as such:
myApp.service('ListService', ListService);
function ListService () {
this.list = ['One', 'Two'];
}
ListService.prototype = {
addItemToList: function (newThing) {
var item = // some initialization of an item from the passed value
this.list.push(item);
}
};
You can then inject ListService anywhere you need access to the data itself. Both the list view and the state2 need it -- the former to actually render the list, and the latter to modify its contents. The method I described above lets you separate the data (the list itself) from the presentation / UI interaction.
[edit]
Likewise, if you want a button that lets you add a new item in both of your views, you could create a directive that receives ListService and prompts the user with a modal when clicked.
I'm working on a project where I use both angularJS and foundation, so I'm making use of the Angular Foundation project to get all the javascript parts of foundation working. I just upgraded from 0.2.2 to 0.3.1, causing a problem in the top bar directive.
Before, I could use a class has-dropdown to indicate a "top-bar" menu item that has a dropdown in it. Since the menu items are taken from a list and only some have an actual dropdown, I would use the following code:
<li ng-repeat="item in ctrl.items" class="{{item.subItems.length > 0 ? 'has-dropdown' : ''}}">
However, the latest version requires an attribute of has-dropdown instead of the class. I tried several solutions to include this attribute conditionally, but none seem to work:
<li ng-repeat="item in ctrl.items" has-dropdown="{{item.subItems.length > 0}}">
This gives me a true or false value, but in both cases the directive is actually active. Same goes for using ng-attr-has-dropdown.
this answer uses a method of conditionally applying one or the other element, one with and one without the directive attribute. That doesn't work if the same element is the one holding the ng-repeat so i can't think of any way to make that work for my code example.
this answer I do not understand. Is this applicable to me? If so, roughly how would this work? Due to the setup of the project I've written a couple of controllers and services so far but I have hardly any experience with custom directives so far.
So in short, is this possible, and how?
As per this answer, from Angular>=1.3 you can use ngAttr to achieve this (docs):
If any expression in the interpolated string results in undefined, the
attribute is removed and not added to the element.
So, for example:
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
angular.module('app', []).controller('testCtrl', ['$scope',
function ($scope) {
$scope.ctrl = {
items: [{
subItems: [1,2,3,4], name: 'Item 1'
},{
subItems: [], name: 'Item 2'
},{
subItems: [1,2,3,4], name: 'Item 3'
}]
};
}
]);
<div ng-app="app">
<ul ng-controller="testCtrl">
<li ng-repeat="item in ctrl.items" ng-attr-has-dropdown="{{ item.subItems.length > 0 ? true : undefined }}">
{{item.name}}
</li>
</ul>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js"></script>
Ok, I made a directive. All <li> will need an initial attr of:
is-drop-down="{{item.subItems.length > 0}}"
Then the directive checks that value and for somereason its returning true as a string. Perhaps some onc can shed some light on that
app.directive('isDropDown', function () {
return {
link: function (scope, el, attrs) {
if (attrs.isDropDown == 'true')
{
return el.attr('has-dropdown', true); //true or whatever this value needs to be
}
}
};
});
http://jsfiddle.net/1qyxrcd3/
If you inspect test2 you will see it has a has-dropdown attribute. There is probably a cleaner solution, but this is all I know. I'm still new to angular.
edit I noticed a couple extra commas in my example json data..take note, still works, but they shouldn't be there.
I'm learning some basics in angularjs and I've somewhat accidentally stumbled across a pattern that works, but I'm not sure why. Also not sure if I'm doing it in an 'angular best practices' kind of way. This example is greatly simplified from what I'm really working on, but the general concept is the same: setting visibility of children based on a toggle on the parent
It seems to my angular-naive self, that it must somehow be watching functions which use the toggledGroups array and then ???. The conditional ng-show does work, I just don't really get why the ng-show="isToggled(group)" gets re-evaluated since the array I'm modifying isn't on $scope.
Do all functions that exist on $scope get re-evaluated on every $digest? If so, this seems to me like it could create a bottleneck on ng-repeats over large data sets.
As I alluded to above, is this an acceptable pattern or is there a better/'more angular' way I should think about this interaction?
JS:
var app = angular.module('demo', []);
app.service('TransportationService', function(){
var groups = [{
group: "planes",
items: ["Airbus A300", "Extra 300S", "Stearman"]
}, {
group: "trains",
items: ["Flying Scotsman", "The Rocket", "Silver Streak"]
}, {
group: "automobiles",
items: ["Veyron", "Vanquish", "FF", "Continental GT"]
}];
return {
groups: groups
}
});
app.controller('TransportationController', ['$scope', 'TransportationService', function($scope, ts){
var toggledGroups = [];
function isToggled(group) {
return toggledGroups.indexOf(group) > -1;
}
function toggleGroup(group) {
var index = toggledGroups.indexOf(group);
if (index > -1) {
toggledGroups.splice(index, 1);
} else {
toggledGroups.push(group);
}
}
$scope.groups = ts.groups;
$scope.isToggled = isToggled;
$scope.toggleGroup = toggleGroup;
}]);
HTML:
<div ng-app="demo" ng-controller="TransportationController">
<div ng-repeat="group in groups">
<h3>{{group.group}}</h3>
<button ng-click="toggleGroup(group)">Toggle</button>
<ul ng-show="isToggled(group)">
<li ng-repeat="item in group.items">{{item}}</li>
</ul>
</div>
</div>
Fiddle: http://jsfiddle.net/wpHZg/1/
Your assumptions are correct. Every time a digest cycle kicks off, it executes that function. You are also correct in the fact that it's more expensive. How much more expensive? Depends on the size of your data set. From the logic you show, I can't imagine it would be too hard on the CPU, but you should consider the fact that it's going to pile up on all of your other application logic. From the way I see it, you could take 2 courses of action to reduce the load on the client CPU.
1) easy: add a toggled property to your groups:
$scope.toggleGroup = function(group) {
group.toggled = !group.toggled
};
<ul ng-if="group.toggled"></ul>
2) More complex: Allows for some DRYness, make a directive! :D
app.directive('toggler', function() {
return {
restrict: 'A',
link: function(scope, elem) {
elem.find('.button-toggler').on('click', function() {
elem.find('.togglee').toggle();
});
}
};
});
<div toggler="">
<button class="button-toggler">Toggle Me Hard!</button>
<ul class="togglee" style="display:none"></ul>
</div>
The second one allows for you to transport your directive to other parts of your code with ease. It also will not kick off a digest cycle as it is all jQuery based (which in this case is good because you're not changing any scope data on click). Either way, you're going to have a cleaner application.
I am struggle with one question and can not figure out why it happens.
I have a module (portfolioModule), service (Menu) and a directive (niMenu). The directive should render a menu item with its subitems.
HTML:
<div ng-app="portfolioModule">
<script id="menuItem" type="text/ng-template">
<li>
<a>{{item.name}}</a>
<ul>
<li ng-repeat="item in menu.getKids(item.id)" ng-include="'menuItem'"></li>
</ul>
</li>
</script>
<div>
<div ni-menu="test">test1</div>
</div>
</div>
JavaScript:
var portfolioModule = angular.module('portfolioModule', []);
portfolioModule.factory('Menu', function() {
var items = [
{
"parent_id": null,
"id": 1,
"name": "foo"
}, {
"parent_id": 1,
"id": 2,
"name": "foo-bar"
}
];
this.getKids = function(parent_id) {
var result = [];
parent_id = parent_id || null;
console.log('getKids', parent_id);
angular.forEach(items, function(value) {
if (value.parent_id === parent_id) {
result.push(value);
}
});
return result;
};
return this;
}
);
portfolioModule.directive('niMenu', function(Menu) {
var niMenu;
return niMenu = {
template: '<ul ng-include="\'menuItem\'" ng-repeat="item in menu.getKids()"></ul>',
restirt: 'A',
link: function(scope, element, attrs) {
console.log('link');
scope.menu = Menu;
}
};
}
);
Working demo: http://jsfiddle.net/VanSanblch/Ng7ef.
In html I call niMenu by ni-menu. The directive has a template and a link function that put Menu-service into a scope of module. In directive's template I use Menu.getKids() and get all top level items. Later, in template that used by ng-include I call Menu.getKids(item.id) and get all children of particular item.
Everything works excellent except one small detail. If you open console then you can observe that there are much more calls of getKids than I am expected. For example, for array of two elements the number of getKids calls is nine.
Could someone explain why on earth that happens?
Ok, so the reason it's executing more than once is because that's how the digest cycle works: it continuously executes all view expressions (like the one you pass to ngRepeat) until it expresses no change. See this answer for more information, but the takeaway is this: it will always execute at least once, but oftentimes more.
When using ngRepeat, you generally want to avoid fetching the data from a function because it negatively impacts performance; why call the same function more than once when the data never changed? A better approach is to have your controller (or in this case directive) execute your function and store the results on the scope, so your ngRepeat looks like ng-repeat="item in items" instead of ng-repeat="item in getItems()".
Unfortunately, that means you have to restructure the way you have your directive working. This turns out to be a good idea anyway, because your directive can be rewritten to be a little bit simpler - you don't need any ngInclude, for example.
What you want to do is create two directives because you have two templates: one to represent the overall menu and one to represent a child item (which, in turn, can have child items). The menu directive should repeat over the top-level menu items, drawing a menuItem for each one. The menuItem directive should check for children and repeat over those, if necessary, with more menuItems. Etc. Here's an example created by Andy Joslin of how you can accomplish a recursive directory: http://plnkr.co/edit/T0BgQR.
To implement something like this moves beyond the scope of this question, but take a stab at it and post a new question if you need help.
Good luck!