AngularJS update controller variable and render the view again? - javascript

I have a webpage with a very simple structure, imagine left navigation, header bar and content area. If I have to describe it with code it looks like this
<body>
<aside ng-controller="SideNavCtrl">Left Nav data</aside>
<header ng-controller="HeaderCtrl">Page Title</header>
<section>
<article ng-view>Page Content</article>
</section>
I'm using ngRoute and change my routes left and right. What I want is to update "Page Title" from the controller that accepted the route. I've tried hundreds of different ways but changing variable never forces header data update. Here's my latest variable share between controllers (which doesn't work)
app.controller("HeaderCtrl", ["$scope", "HeaderData", function($scope, HeaderData) {
$scope.title = HeaderData.title();
}]);
app.factory("HeaderData", function() {
var title = 'Default';
return {
title: function() { return title; },
setTitle: function(newTitle) { title = newTitle; }
}
});
Later on in the routed controller I go like this
app.controller("PreviewBuildCtrl", ["$scope", "$routeParams", "$location", "BuildsAPIService", "HeaderData", function($scope, $routeParams, $location, BuildsAPIService, HeaderData) {
$scope.build = BuildsAPIService.getBuildById($routeParams.buildId);
HeaderData.setTitle("Previewing Build");
console.log(HeaderData);
if (!$scope.build) {
$location.url('/builds/' + $routeParams.profession);
}
}]);
The problem is that using HeaderData.setTitle() doesn't update the header contents. I really don't want to write jQuery code for this task. There must be a smarter, more angular way of doing this.
All I want to do is change the header title to "Page B" when you move to page b from page a and I consider adding breadcrumbs plugins an overkill for the task at hand :(

You break the data binding via title = newTitle. The controllers are pointing to the old title, but HeaderData is not pointing to it anymore so it is lost. You need another dot.
this.title = newTitle;
The title method should also return this.title, of course.

Related

Run JS script after Angular has retrieved data

I'm using the GridLoadingEffects library by Codrops along with Angular JS to feed it list items with data from wordpress. Everything works in terms of Angular retrieving the data via $http request and storing the variables asynchronously. The data is processed and binded to HTML via ng-bind-html.
<body ng-app="myApp">
<div class="container" ng-controller="myController">
<ul class="grid effect-6" id="grid" ng-bind-html="bindHTML(html_code)"></ul>
</div>
</body>
After a slight delay as a result of retrieving the data, I can see that the HTML is in fact loaded into the DOM in the inspector. However, the problem is that the GridLoadingEffects effect immediately sees that there are no list items in the DOM, and therefore throws an undefined error.
I'm not an experienced web developer, so I can't precisely determine where the call is made to check for the DOM's li elements and apply the effect, and this library has many dependencies including modernizr.js, masonry.js, imagesloaded.js, AnimOnScroll.js, and more. What confuses me further is that these files are loaded in different places in the HTML, some in the head and some at the end of the body. So in terms of the execution flow, I'm really lost as to how to solve this problem. I'm not sure but I think this function is what instantiates the effect.
<script>
window.onload = function() {
new AnimOnScroll( document.getElementById( 'grid' ), {
minDuration : 0.4,
maxDuration : 0.7,
viewportFactor : 0.2
} );
}
</script>
It is to be placed at the end of the body, as per the example html file provided by Codrops. I think that in order to rectify the issue, the function above has to run once Angular has fed the data to the ul tag. Even though there's a window.onload attached to it, it probably doesn't consider that there's more to load with angular.
Am I correct in my assumptions? I'm sure there's an easy fix to this. Can anyone help?
UPDATE:
Here is how the $http request is initiated:
(function() {
'use strict'
angular.module('myApp', [])
.controller('myController', myController);
myController.$inject = ['$scope', '$sce','$http']
function myController ($scope, $sce, $http) {
$http({
method : "GET",
url : myURL
})
.then(function(response) {
$scope.myData = response.data;
var list = new Array
for (var i = 0; i < $scope.myData.length; i++) {
var string = '<li><figure class="blurfilter semitransparent"><img src=myURL><figcaption><h3>' + $scope.myData[i]['title']['rendered'] + '</h3></figcaption></figure></li>';
list.push(string)
}
$scope.html_code = list.join("")
$scope.bindHTML = function(html_code) {
return $sce.trustAsHtml(html_code)
};
});
};
})();
UPDATE 2:
My HTML now using ng-repeat to separate model and view:
<li ng-repeat="(key, val) in myData">
<h3>{{val.title.rendered}}</h3>
</li>
And my angular code (omitted the http requests):
(function() {
'use strict'
angular.module('myApp', [])
.controller('myController', myController);
myController.$inject = ['$scope', '$sce','$http']
function myController ($scope, $sce, $http) {
$scope.$watch('myData', function() {
// if the myData is loaded
if ($scope.myData.length > 0) {
new AnimOnScroll( document.getElementById( 'grid' ), {
minDuration : 0.4,
maxDuration : 0.7,
viewportFactor : 0.2
});
}
});
Now I get $scope.myData is undefined. $scope.myData is defined in the .then of my http request. Maybe it's helpful to mention that this error points to a line in the angular.min.js file, not to my app.js.
You can try to use $scope.$watch function in your angular controller on the data variable after you get the data using $http service.
$scope.$watch('myData', function() {
// if the myData is loaded
if ($scope.myData && $scope.myData.length > 0) {
// your code
}
});
I understand my problem deviated a little from running a function after data has been retrieved to running the function after the retrieved data has populated the DOM. For anyone stuck on the same problem (not when the data is retrieved but after retrieved and DOM has been populated), you need to use directives. Check this post: ng-repeat finish event. Your solution is here.

Variables not updating outside function

So, I have two controllers (they have been reduced for simplicity) with one function each. The functions change a variable inside each of the controllers. The thing is, it looks like only the variable in the first controller is updated.
app.js
(function() {
var app = angular.module('ScBO', []);
var token = {};
app.controller("LoginController", ['$http', function($http){
this.userData = {};
var lc = this;
this.in = false;
this.loginGo = function(){
$http.post(<link>, <userData>)
.then(function successCallback(response){
token = response.data.access_token;
lc.in=true;
}, function errorCallback(response){
lc.in=false;
});
};
}]);
app.controller("DashboardController", ['$http', function($http){
var dsb = this;
this.totalLocals = 0;
this.refresh = function(){
$http.get('<link>?access_token='+token)
.then(function successCallback(response){
dsb.totalLocals = response.data.number_of_locals.number_of_locals;
}, function errorCallback(response){
});
};
}]);
})();
index.html
<body ng-controller="LoginController as loginCtrl">
<div id="login-div" ng-hide="loginCtrl.in">
<form ...>
...
</form>
</div>
<div id="dashboard" ng-show="loginCtrl.in" ng-controller="DashboardController as dsb">
<div class="numbers">
<p>Number of Locals</p>
{{dsb.totalLocals}}
</div>
</div>
</body>
So in the html, the first div appears in the beginning and not the second one. Then it hides and the second div shows up. This means that they are following the update of the loginCtrl.in variable.
However, when I call the refresh function, it doesn't update the value in the last div.
I've tried with $scope and have been searching in here but I haven't been able to find a solution yet. Specially since the two controllers are equal but only one of them seems to be updating the variables normally.
So, I've figured out the problem.
In the code above the error doesn't show as I only posted part of the code for simplicity, but I thought it was enough.
The thing was: I had the button that triggers the refresh function in a different div (like #leonardoborges ' plunkr). Like that two controllers are instantiated with their own values, something that I didn't know. The timeout function updates all instances of the controller, so that was confusing me.
So now I just put everything within one instance of the controller.

AngularJS create slidedown menu

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); } );
});

Refreshing contents of div based on drop down selection (custom directive) in Angularjs

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.

Dynamic menu bar with angularjs

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;
});
}
}
};
}]);

Categories