I'm building an AngularJS app, and basically I have a menu on top of my page then when I select an item is supposed to slide down some content of my page and show the menu in that area. I've read something about creating a directive but isn't still clear to me how can I do it. I show an example of my page:
Then when I select an item from the Menu, the static image disapears and gives place to a "sub menu" and the sub menu content itself.
In some cases, where the "SubMenu" + "Content" height is bigger then the height of the image, the rest of the content will slide down.
My main concern is how to show this SubMenu depending on the menu selected (in the black square). My idea was to create some menu template (with all the html and css created) and then just bind the different content to this template and show it on the div that I want (in this case the div that started with the static image). But since AngularJS is new to me I'm having some problems to put it in pratice.
You can see plunker example here:
https://plnkr.co/edit/xJoF4IuDtJlGzWmTYoqZ?p=preview
And here is the angular code explanation:
angular.module('plunker', []);
function MainCtrl($scope, menuSvc) {
// main controller sets the menu contents to the service
menuSvc.putAll([{t:"item1"},{t:"item2"}]);
}
// a very simple menu service that keeps an object of menu items
angular.module('plunker').factory("menuSvc", [ function( ) {
var items;
var clear = function(){
items = {};
};
var getAll = function(){
return Object.keys(items);
};
var put = function( item ){
items[item.t] = item;
};
var putAll = function( itemArray, dontClean ){
if( !dontClean ){
clear();
}
itemArray.forEach(
function(i){
put(i);
}
);
};
clear();
return {
put: put,
getAll: getAll,
putAll: putAll,
clear: clear
};
}]);
// directive that gets its content from the service
angular.module('plunker').directive('menu', function(){
return {
restrict: 'E',
scope: {
},
templateUrl: 'menu.html',
controller: function($scope, menuSvc) {
$scope.menu = menuSvc.getAll();
},
replace: true
};
});
When content of the menu is updated you may communicate this event to the directive via angular events, so that it will re-read the cotnent fromt he service. See (https://docs.angularjs.org/api/ng/type/$rootScope.Scope) for $on, $emit, $broadcast
Example of a listener at menu controller:
$scope.$on('someEvent', function(){
// update menu content
// trick is that you have to update the menu content, not to overwrite it
// for example like this:
$scope.menu.length = 0;
var newValues = menuSvc.getAll();
newValue.forEach(function(x){ $scope.menu.push(x); } );
});
Related
I have a custom directive which is very similar to a drop down. Items in the drop down menu are associated with file names of certain videos. A div below the drop down displays a default video file (I have done this via Videoangular).
Whenever I make a selection from the drop down menu, I am changing the default variable containing filename (String) to the one I want. But, the same is not reflected in the div.
My objective is to refresh div containing the video with appropriate video whenever a selection is made from the drop down menu.
This is my controller:
angular.module('myApp',
[
"ngSanitize",
"com.2fdevs.videogular",
"com.2fdevs.videogular.plugins.controls",
"com.2fdevs.videogular.plugins.overlayplay"
]
)
.controller('ctrl',
["$rootScope", "$scope", "$state", "$log", "Restangular",
function ($rootScope, $scope, $state, $log, Restangular) {
'use strict';
var vm = this;
//DEFAULT VIDEO FILE NAME
vm.fields = "WaterCool1";
this.config = {
sources: [
{src: "assets/data/"+vm.fields+".mp4", type: "video/mp4"}
]
};
vm.loadPage = loadPage;
vm.coolingSystemTypeSelector = {coolingSystemTypeSelector:{}};
getAll('cooling-system-type').then(
function(objs) {
$log.debug("get Cooling System Type", objs);
vm.coolingSystemTypeSelector = objs.selector;
vm.fields = "WaterCool1";
vm.coolingSystemTypeSelector.onSelect = function (selection) {
if(!selection){
return;
}
$log.debug("Cooling System Type Selection == ", selection);
if(selection.label==="ACC"){
vm.fields = "AirCool";
}else if(selection.label === "WCC-CT"){
vm.fields = "WaterCool1";
}else if(selection.label === "WCC-DC"){
vm.fields = "WaterCool2";
}
};
}
);
///.....
}
]
);
This is my HTML:
<div>
<selector form="form" columns=vm.columns target="vm.coolingSystemTypeSelector"></selector>
</div>
<hr>
<div id="refreshThisDiv">
<!--I want to refresh this div-->
<videogular vg-theme="vm.config.theme">
<!--VIDEOGULAR CODE-->
</videogular>
</div>
What you need is not to refresh the div. You need angular to refresh the div based on you modifying bound properties.
Your declaration of this.config is actually static and you are never modifying the value of this.config.sources src after instantiation. As that code is running only once it will forever remain as "assets/data/WaterCool1.mp4".
What you need to do instead at least, is to modify this value upon selection of an option in the drop-down. Something like:
// ...
var that = this;
getAll('cooling-system-type').then(
// ... inside onSelect ...
if(selection.label==="ACC") {
that.config.sources = [{src: "assets/data/AirCool.mp4", type: "video/mp4"}];
}
// ...
Even then, with this code, you might need to trigger a manual $apply as angular may not be aware of your change to the field via this onSelect event handling. Ideally you will be able to bind the event to the function directly in HTML by using ng-change and avoid the need for that.
If you provide a full sample (https://plnkr.co/edit/), it's easier to guide you to a solution and explain in without the need to rewrite your original code.
First what I want:
I have a series of thumbnails. When I click on one, I want that specific thumbnail to be shown in a bigger div, with its description. (For later: should be animated).
Now I have the directive and the controller, but I don't know how to set the appropriate variable!
So some code:
First HTML: here is the root .jade file for this section. I have a directive called product.
section
.detail-view(ng-show="vm.showDetail")
.prod-desc(ng-bind="vm.detailPic")
.prod-img(ng-bind="vm.detailDesc")
product.col-xs-12.product_tile(ng-repeat="item in vm.products", item="::item")
As you can see, the product directive is part of an ng-repeat; for this reason, the div I want to show the resized image is outside the iteration (.detail-view).
The product directive:
'use strict';
ProductDirective.$inject = ['ShopCart', '$animate', 'User'];
function ProductDirective(ShopCart, $animate, User) {
return {
restrict: 'E',
scope: {
item: '='
},
template: require('./product.html'),
link: link
};
function link(scope, element) {
scope.toggleDetails = toggleDetails;
}
function toggleDetails() {
if (!scope.isSelected && isBlurred()) return;
scope.vm.detailPic = scope.item.photo;
scope.vm.detailDesc = scope.item.prod_description;
scope.vm.isSelected = !scope.isSelected;
scope.showDetail = !scope.showDetail;
var action = scope.isSelected ?
}
}
Now the div I want to update with the image in big is outside the iteration - and hence outside the scope of the directive. How can I set the value of showDetail, showDesc and showPic?
As I am using controllerAs with value vm, I thought I could just do scope.vm.detailPic = scope.item.photo;, as in other solutions I have seen that when setting a property on a root object, it would be propagated...but I get
Cannot set property 'detailPic' of undefined
For now, what works (but looks a bit odd to me) is this, in toggleDetails()
scope.$parent.$parent.vm.detailPic = scope.item.photo;
scope.$parent.$parent.vm.detailDesc = scope.item.prod_description;
scope.$parent.$parent.vm.showDetail = !scope.$parent.$parent.vm.showDetail
http://plnkr.co/edit/39FGMocKB5GtQWnI1TFw?p=preview
I have sidebar which contains a list of tags, when you click on a tag I use the TagDetailsFactory to send a tag into the scope of the view controller.
Everything works great except for when you hover over a tag in the TagDetailsFactory scope.
The tagDetails template does not show up, however if you hover over the same tag in the sidebar scope, the tagDetails shows up in both. This is wrong.
Hovering over a tag in the sidebar should only show the tag details for that tag and the same for the tags inside the view scope.
Hovering over a tag in the view scope, doesn't display it's details
Hovering over a tag inside of the sidebar scope, should only show details for it's tag, and not the tag in the view scope, like it does here:
Steps:
- The first tags Array is in the cnt controller
- When you click on a tag, it gets stored in the TagDetailsFactory service
- I then broadcast an event to the view controller to then call the getTagDetails function in TagDetailsFactory to retrieve the saved tags and store them into the viewTags array in the view controller.
// Code goes here
angular.module('app', [])
.directive('tagDetails', function() {
return {
restrict: "E",
link: function($scope, el, attrs) {
// console.debug($scope, attrs);
},
scope:{
tag:'='
},
template: '<div ng-show="tag.showDetails">{{tag.details}}</div>'
};
})
.factory('TagDetailsFactory', function() {
var savedTags = [];
var saveTagDetails = function(tag) {
savedTags.push(tag);
}
var getTagDetails = function() {
return savedTags;
}
return {
saveTagDetails : saveTagDetails,
getTagDetails : getTagDetails
};
})
.controller('sidebar', function($scope,
$rootScope,
TagDetailsFactory) {
$scope.tags = [];
for(var i = 0; i < 10; i++) {
$scope.tags.push(
{ name: 'Foo Bar ' + i, details: 'Details' + i }
);
}
$scope.showTagDetails = function(t) {
t.showDetails = true;
}
$scope.leaveTag = function(t) {
t.showDetails = false;
}
$scope.sendTag = function(t) {
TagDetailsFactory.saveTagDetails(t);
$rootScope.$broadcast('updateView');
}
})
.controller('view', function($scope,
$rootScope,
TagDetailsFactory) {
$scope.viewTags = [];
$scope.$on('updateView', function() {
$scope.viewTags = TagDetailsFactory.getTagDetails();
});
$scope.showTagDetails = function(v) {
v.showDetails = true;
}
$scope.leaveTag = function(v) {
v.showDetails = false;
}
});
Do I have to create a 2nd directive here? To be the template for the tag details in the view scope? Or can my current tagDetails directive be repurposed somehow in an Angular way?
I forked your Plunker with a working copy, and I'll explain the changes I made.
You have two issues with the code here. The first is a simple typo, which is causing your header to not reference the correct function for mouseover. Your functions are calling showTagDetailsView(v)and leaveTagView(v), but they are named showTagDetails and leaveTag on the controller.
The second issue is with the way that the items are added to the savedTags[]. In JavaScript, objects are passed by reference. when you call savedTags.push(tag);, you are pushing a reference to the same object into the new array. Any changes made to the object in one array will be reflected in the other array.
Instead, what you want is a separate copy of the object in the savedTags[]. This can be accomplished by using angular.copy. Note that I also reset tag.showDetails = false; before making the copy, else the new copy will have it set to true, and the details will be showing the instant the copy appears, even though you are hovering over the other element when you click it.
var saveTagDetails = function(tag) {
tag.showDetails = false;
savedTags.push(angular.copy(tag));
}
Just a side note, you might also have an issue with CSS here, as hovering seems to change the position of the lists, and in some cases the hover actually causes the tag to move itself out of the hover, causing a bounce effect.
I have very little javascript experience. I need to add a menu on click of an item. We have been asked to build it from scratch without using any library like bootstrap compoments or JQuery.
We are using Angularjs. In angular I want to know the correct method to create new elements. Something like what we did not document.createElement.
I am adding some of the code for you guys to have a better idea what I want to do.
Menu Directive
.directive('menu', ["$location","menuData", function factory(location, menuData) {
return {
templateUrl: "partials/menu.html",
controller: function ($scope, $location, $document) {
$scope.init = function (menu) {
console.log("init() called");
console.log("$document: " + $document);
if (menu.selected) {
$scope.tabSelected(menu);
}
}
$scope.creteMenu = function(menuContent){
//This is to be called when the action is an array.
}
$scope.tabSelected = function(menu){
$location.url(menu.action);
$scope.selected = menu;
}
$scope.click = function (menu) {
if (typeof (menu.action) == 'string') {
$scope.tabSelected(menu);
}
}
},
link: function (scope, element, attrs) {
scope.menuData = menuData;
}
};
}])
Menu data in service.
.value('menuData', [{ label: 'Process-IDC', action: [] }, { label: 'Dash Board', action: '/dashboard', selected: true }, { label: 'All Jobs', action: '/alljobs', selected: false }, { label: 'My Jobs', action: '/myjobs', selected: false }, { label: 'Admin', action: '/admin', selected: false }, { label: 'Reports', action: '/reports', selected: false }]);
If you notice the action of Process-IDC menu is an array it will contain more menu with actions in it and it should be opened in a sub menu.
Menu.html (partial)
<ul class="menu">
<li ng-class="{activeMenu: menu==selected}" ng-init="init(menu)" data-ng-click="click(menu)" data-ng-repeat="menu in menuData">{{menu.label}}</li>
</ul>
A few things come to mind. First of all, are you sure you need to actually create the element on click? If you are doing to to show a fixed element on click then the better approach would be to generate the element as normal, but not show it until you click. Something like:
<div ng-click="show_it=true">Show item</div>
<div ng-show="show_it">Hidden until the click. Can contain {{dynamic}} content as normal.</div>
If you need it to be dynamic because you might add several elements, and you don't know how many, you should look at using a repeat and pushing elements into a list. Something like this:
<div ng-click="array_of_items.push({'country': 'Sparta'})">Add item</div>
<div ng-repeat="item in array_of_items"> This is {{item.country}}</div>
Each click of the "Add item" text here will create another div with the text "This is Sparta". You can push as complex an item as you want, and you could push an item directly from the scope so you don't have to define it in the template.
<div ng-click="functionInControllerThatPushesToArray()">Add item</div>
<div ng-repeat="item in array_of_items"> This is {{item.country}}</div>
If neither of those options would work because it is a truly dynamic object, then I would start looking at using a directive for it like others have suggested (also look at $compile). But from what you said in the question I think a directive would be to complicate things needlessly.
I recommend you read the ngDirective and the angular.element docs.
Hint: angular.element has an append() method.
This is both really simple, but some what complex if you don't know where to start - I really recommend looking at the Tutorial, and following it end to end: http://docs.angularjs.org/tutorial/ - As that will introduce you to all the concepts around Angular which will help you understand the technical terms used to describe the solution.
If you're creating whole new menu items, if in your controller your menu is something like:
// An Array of Menu Items
$scope.menuItems = [{name: 'Item One',link: '/one'},{name: 'Item Two',link:'/two'}];
// Add a new link to the Array
$scope.addMenuItem = function(theName,theLink){
$scope.menuItems.push({name: theName,link:theLink});
}
And in the template, use the array inside ng-repeat to create the menu:
<ul>
<li ng-repeat="menuItem in menuItems">{{menuItem.name}}</li>
</ul>
If you just want to toggle the display of an item that might be hidden, you can use ng-if or ng-show
Assuming that you are doing it in a directive and you have angular dom element, you can do
element.append("<div>Your child element html </div>");
We can use $scope in App Controller to create Div Elements and then we can append other Div elements into it similarly.
Here's an Example:
$scope.div = document.createElement("div");
$scope.div.id = "book1";
$scope.div.class = "book_product";
//<div id="book1_name" class="name"> </div>
$scope.name = document.createElement("div");
$scope.name.id = "book1_name";
$scope.name.class= "name";
// $scope.name.data="twilight";
$scope.name.data = $scope.book.name;
$scope.div.append($scope.name);
console.log($scope.name);
//<div id="book1_category" class="name"> </div>
$scope.category = document.createElement("div");
$scope.category.id = "book1_category";
$scope.category.class= "category";
// $scope.category.data="Movies";
$scope.category.data=$scope.book.category;
$scope.div.append($scope.category);
console.log("book1 category = " + $scope.category.data);
//<div id="book1_price" class="price"> </div>
$scope.price = document.createElement("div");
$scope.price.id = "book1_price";
$scope.price.class= "price";
// $scope.price.data=38;
$scope.price.data=$scope.book.price;
$scope.div.append($scope.price);
console.log("book1 price = " + $scope.price.data);
//<div id="book1_author" class="author"> </div>
$scope.author = document.createElement("div");
$scope.author.id = "book1_author";
$scope.author.class= "author";
// $scope.author.data="mr.book1 author";
$scope.author.data=$scope.book.author;
$scope.div.append($scope.author);
console.log("book1 author = " + $scope.author.data);
//adding the most outer Div to document body.
angular.element(document.getElementsByTagName('body')).append($scope.div);
For more illustration, Here each book has some attributes (name, category, price and author) and book1 is the most outer Div Element and has it's attributes as inner Div elements.
Created HTML element will be something like that
I'm trying to create a menu bar using Angularjs. I've done similar things before with Backbonejs, but I have a hard time getting my head around how to do this with angular.
In my html file, I have the following menu placeholder.
<div id='menu1'></div>
<div id='menu2'></div>
<div id='menu3'></div>
<div id='menu4'></div>
<div id='menu5'></div>
A number of my angular modules add a menu when they are loaded (in run). Each of them only reserves a particular slot (i.e. menu1..5), so they don't clash. When some modules aren't loaded, their menu would not show in the menu bar.
An angular module would conceptually look like:
angular.module('myModule3', [])
.service('someService', function($http) {
// get some data to populate menu (use $http)
this.menuItems = ['orange', 'apple', 'banana']
})
.run(['someService', function(someService) {
// create a rendered menu item
...
// insert it at id="menu3"
})
For sake of simplicity, the rendered menu item should look like:
<ul>
<li>organge</li>
<li>apple</li>
<li>banana</li>
</ul>
I'm fairly new to angular, so I don't really know where to begin. I've been reading up on directives, but don't see how they fit in here, as they require some custom markup (maybe a custom menu tag containing the DOM target (i.e. menu..5). Also, how to connect this to a controller is not clear to me.
Update
In addition to the above base template (containing arbitrary anchor points in the DOM) and the directive (which will produce a DOM element which will be inserted at these anchor points), a template will facilitate the creation of the DOM element. This template will be located in a separate file containing the position the directive's DOM element will be inserted to (as opposed to the usual case of directives in which an already existing tag will be replaced/inserted into specific markup that matches the directive's definition:
<menu ng-model="Model3DataService" target="#menu3">
<ul>
<li ng-repeat="for item in items"></li>
</ul>
</menu>
Again, coming from a Backbone/jquery background this makes sense, but this may not be the right thing to do in angular. If so, please let me know how I could keep the base template free of any knowledge about the modules and assumptions of where they put their menu (i.e. which slot of the menu bar they allocate). I'm happy to hear about other solutions...
Each module should have its menu loader defined:
angular.module('module1', []).
factory('module1.menuLoader', function() {
return function(callback) {
callback(['oranges', 'bananas'])
}
});
Your application should contain menu directive which can load menu items for any module only if exists.
angular.module('app', ['module1']).
directive('menu', ['$injector', function($injector) {
return {
restrict: 'A',
template:
'<ul><li ng-repeat="item in items">{{item}}</li></ul>',
scope: {},
link: function($scope, $element, $attrs) {
var menuLoaderName = $attrs.menu+'.menuLoader';
if ($injector.has(menuLoaderName)) {
var loaderFn = $injector.get(menuLoaderName);
loaderFn(function(menuItems) {
$scope.items = menuItems;
});
}
}
};
}]);
Final html:
<div class="content">
<div menu="module1"></div>
<div menu="module2"></div>
<div menu="module3"></div>
</div>
After running the application only module1 menu will be loaded. Other menu placeholders remain empty.
Live demo: http://plnkr.co/edit/4tZQGSkJToGCirQ1cmb6
Updated: If you want to generate markup on the module side the best way is to put the template to the $templateCache in the module where it's defined and then pass the templateName to the application.
angular.module('module1', []).
factory('module1.menuLoader', ['$templateCache', function($templateCache) {
$templateCache.put('module1Menu', '<ul><li ng-repeat="item in items">{{item}}</li></ul>');
return function(callback) {
callback('module1Menu', ['oranges', 'bananas'])
}
}]);
angular.module('app', ['module1'])
.directive('menu', ['$injector', function($injector) {
return {
restrict: 'A',
template:
'<div ng-include="menuTemplate"></div>',
scope: {},
link: function($scope, $element, $attrs) {
var menuLoaderName = $attrs.menu+'.menuLoader';
if ($injector.has(menuLoaderName)) {
var loaderFn = $injector.get(menuLoaderName);
loaderFn(function(menuTemplate, menuItems) {
$scope.menuTemplate = menuTemplate;
$scope.items = menuItems;
});
}
}
};
}]);