I have a common module with a controller, component and template combo for initialisation purposes and defining the base layout of the app. I then have a stateful component that on initialisation makes a HTTP GET requests to fetch a background image from an API. Once the promise is resolved, I use $rootScope.$emit to fire an event back up to the common controller with the image URL so that it can be set as the background image of the app dynamically.
What I can't get my head around is that when console logging the event response inside the $rootScope.$on() function shows me the result, but anywhere else inside in the controller yields nothing (inside $onInit() or anywhere else).
What's more baffling is that I can render the event data in the Common controller's template no problem, be it inside an input box with ng-model or inside a paragraph. When I try to concatenate it as part of my inline CSS or background-image directive however, it's not picked up whatsoever. The directive returns the URL up to the variable name (https://image.tmdb.org/t/p/original) then cuts off.
Any suggestions would be really really appreciated!
This is the component controller code:
function MovieDetailController(MovieService, $rootScope){
var ctrl = this;
ctrl.$onInit = function(){
// Get all additional data
MovieService
.getAdditionalData(ctrl.movie.movie_id)
.then(function(response){
ctrl.actors = response.credits.cast.splice(0, 6);
ctrl.extras = response.videos.results.splice(0, 3);
ctrl.similar = response.similar.results.splice(0,6);
ctrl.backdrop = response.backdrop_path;
$rootScope.$emit('backdropChange', ctrl.backdrop);
});
}
}
angular
.module('components.movie')
.controller('MovieDetailController', MovieDetailController)
And this is the Common Controller code
function CommonController($state, $rootScope){
var ctrl = this;
$rootScope.$on('backdropChange', function(event, data){
ctrl.backdrop = data;
// Displays result successfully
console.log('Back drop is ' + ctrl.backdrop);
});
ctrl.$onInit = function(){
// Doesn't log result
console.log('On Init, Backdrop is ' + ctrl.backdrop);
}
}
angular
.module('common')
.controller('CommonController', CommonController);
Here is the HTML template for the Common Controller
<header class="row" id="topnav">
<topnav class="col-sm-12 p-3 d-inline-flex"></topnav>
</header>
<!-- Testing if data is rendered inside the input box and it is! -->
<div class="col-sm-12"><input type="text" name="" ng-model="$ctrl.backdrop"></div>
<!-- Directive only receives first part of URL up to variable then cuts off-->
<main class="row" id="main-body" back-img="https://image.tmdb.org/t/p/original{{$ctrl.backdrop}}">
<aside class="flex-fixed-width-item bg-white" id="sidebar">
<sidebar></sidebar>
</aside>
<section class="col" id="content-window">
<!-- Filters and Main Section Submenu -->
<div class="row">
<nav class="col-sm-12 filter-container">
<div ui-view="details-menu"></div>
</nav>
</div>
<!-- Main Content Section -->
<div ui-view="details" class=""></div>
</section>
</main>
Last but not least the background image directive
function backImg(){
return {
link: function(scope, element, attrs){
var url = attrs.backImg;
element.css({
'background-image': 'url(' + url +')'
});
}
}
}
angular
.module('common')
.directive('backImg', backImg);
The backImg directive should $observe the attribute:
function backImg(){
return {
link: function(scope, element, attrs){
attrs.$observe("backImg", function(url) {
element.css({
'background-image': 'url(' + url +')'
});
});
}
}
}
From the Docs:
$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest following compilation. The observer is then invoked whenever the interpolated value changes.
— AngularJS Attributes API Reference - $observe
Related
I have been tasked with developing an app in Angular 1.6, and I have not done any Angular 1.x for quite some time. Been mostly doing 2.x. In fact, never done 1.6 at all.
I have two components: a tile container, and a tile. The tiles are selectable, so I imagined having a tile container component that would keep track of which tile was selected, and a tile component which is primarily a UI component.
My tile component looks like this:
// dp-claim-filter-tile.component.js
function ClaimFilterTileController() {
var ctrl = this;
ctrl.isDisabled = () => {
return ctrl.claimCount == 0;
}
ctrl.test = function () {
console.log('ctrl = ' + JSON.stringify(ctrl));
ctrl.onTileClicked(ctrl);
}
}
angular.module('dpApp').component('dpClaimFilterTile', {
templateUrl: '/templates/dp-claim-filter-tile.tmpl.html',
controller: ClaimFilterTileController,
bindings: {
tileTitle: '#',
claimCount: '#',
isAdd: '<',
isActive: '<',
onTileClicked: '&'
}
});
And the template looks like this:
// dp-claim-filter-tile.tmpl.html
<div ng-if="$ctrl.isAdd===true" layout="column" layout-align="space-around center">
<div><h2 class="md-title">{{$ctrl.tileTitle}}</h2></div>
<div>
<md-icon md-font-icon="fa-plus" class="fa fa-2x"></md-icon>
</div>
</div>
<div ng-if="$ctrl.isAdd!==true" layout="column" layout-align="space-around center" ng-click="$ctrl.test()">
<div><h2 class="md-title tile-text">{{$ctrl.tileTitle}}</h2></div>
<div class="md-display-1 tile-text">{{$ctrl.claimCount}}</div>
</div>
The tile container component looks like this:
// dp-claim-filter-tiles.component.js
function ClaimFilterTilesController() {
var ctrl = this;
ctrl.tileClicked = function(tile) {
console.log('tile = ' + JSON.stringify(tile));
}
}
angular.module('dpApp').component('dpClaimFilterTiles', {
templateUrl: '/templates/dp-claim-filter-tiles.tmpl.html',
controller: ClaimFilterTilesController
});
And an extract of the container UI looks like this:
// dp-claim-filter-tiles.tmpl.html
<md-grid-tile>
<dp-claim-filter-tile is-active="true" tile-title="Links Sent" claim-count="7" on-tile-clicked="$ctrl.tileClicked($event)"></dp-claim-filter-tile>
</md-grid-tile>
What I am expecting, and hoping for, is for the $event parameter I am supplying to surface as a parameter to the ClaimFilterTilesController.tileClicked() function.
How do I accomplish this?
The click $event will be available in the ngClick directive by itself, you won't be able to carry it around before the actual ngClick directive gets triggered.
Therefore, you could access the click event exclusively in dp-claim-filter-tile.tmpl.html template, like this:
ng-click="$ctrl.test($event, otherParam)"
Try utilizing ng-click rather than the on-tile-clicked. This way you can get at the $event object which can be passed into other functions within your component definition.
<md-grid-tile>
<dp-claim-filter-tile-is-active="true" tile-title="Links Sent" claim-count="7" ng-click="$ctrl.tileClicked($event)"></dp-claim-filter-tile>
</md-grid-tile>
I found the answer on the AngularJS site: Intercomponent Communication. The trick lies in using the require property when defining the component. You can essentially create a reference to the enclosing component. Shall implement tomorrow.
Thanks for your answers guys.
This is my first post/question here on StackOverflow so if you see any improvement I can made - please give me an advice :).
Now let me dive into the issue.
For the sake of simplicity I removed any irrelevant portions of code and presented only necessary files.
//app.modules.js
if (typeof window.app == "undefined") {
window.app = angular.module("AppModule", []);
}
//app.services.js
window.app
.service("settingsOverlaySvc", function($rootScope) {
this.broadcastToggle = function() {
$rootScope.$broadcast("toggle-conf");
};
});
//settings-ctrl.js
window.app.controller("SettingsController", ["$scope", "$window", "$sce", "settingsOverlaySvc",
function($scope, $window, $sce, settingsOverlaySvc) {
$scope.visible = false;
$scope.open = false;
$scope.toggleSettings = function() {
$scope.open = !$scope.open;
};
$scope.broadcastToggle = function() {
settingsOverlaySvc.broadcastToggle();
};
$scope.$on("toggle-conf", function() {
console.log("toggle-conf received");
$scope.visible = !$scope.visible;
});
}
]);
angular.bootstrap($("div[ng-controller='SettingsController']").parent(":not(.ng-scope)"), ["AppModule"]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<!-- Appended by JS - control1.html -->
<div>
<div ng-controller="SettingsController" ng-init="visible=true">
<span class="glyphicon glyphicon-cog" ng-class="{'settings-open': open}" ng-click="broadcastToggle();toggleSettings()">COG</span>
</div>
</div>
<!-- Appended by JS - control2.html-->
<div>
<div ng-controller="SettingsController" ng-cloak>
<div ng-if="visible">
<span class="glyphicon glyphicon-cog" ng-class="{'settings-open': open}" ng-click="toggleSettings()">COG</span>
<div ng-if="open">
<div class="divControl_2">Content_2</div>
</div>
</div>
</div>
</div>
The above snippet works as I expected - the $broadcast is called, all controllers receive the message and reacts. In my application the broadcast is received only by controller sending it. I think the problem is caused by dynamic HTML insertions.
The requirement is to render controls dynamically on page load. I'm using the following approach to generate content.
AddWidgets: function () {
var controlContainer1 = $("<section>", {
class: "sidebar-section",
id: "ctrl1-container"
});
var controlContainer2 = $("<section>", {
class: "sidebar-section",
id: "ctrl2-container"
});
$("aside.sidebar").append(controlContainer1);
$("aside.sidebar").append(controlContainer2);
$("#ctrl1-container").load("..\\assets\\ctrls\\control1.html");
$("#ctrl2-container").load("..\\assets\\ctrls\\control2.html");
}
I'm using the same controller because it shares the same logic for all controls.
I've read a lot materials about $broadcast and $emit functionality, guides on creating controllers, defining modules and services (that one gives me idea about creating service with $rootScope injected).
Now I'm thinking that generating angular content outside angular framework (AddWidgets function) can cause the problem (#stackoverflow/a/15676135/6710729).
What raised my spider sense alarm is when I've checked the JSFiddle example for the similar situation (http://jsfiddle.net/XqDxG/2342/) - no parent-child relation of controllers. When I peek at angulat scope of the controllers I can see that $$nextSibling and $$prevSibling properties are filled. In my case these are nulled [here].
Can you give me some guidelines how can I resolve my issue? I'm fairly new to AngularJS and learning as I'm developing the application.
I'm working on a site, and I started building it before I realized I needed some dynamic framework. After learning about AngularJS, I decided to use it, where I needed (not the whole site).
I have a very long script in JS, and I want to be able to get and set the variables from within AngularJS directives and controllers.
I found this answer, and it was quite good - I was able to get the variable from within the function. But when the variable changed outside the function, AngularJS' variable won't update.
My code looked something like this:
JS:
var app = angular.module('someName', []);
var currentPage = 'Menu';
app.controller('PageController', ['$window','$scope', function($window,$scope){
this.currentPage = $window.currentPage;
this.isPage = function(page){
return (page == this.currentPage);
};
}]);
function button1onClick(){
currentPage = 'Game';
}
HTML:
<div ng-controller="PageController">
<div id="Game" ng-show="page.isPage('Game')">
...
</div>
<div id="Menu" ng-show="page.isPage('Menu')">
...
</div>
</div>
(button1onClick was called when I clicked some button on the page)
The idea is that I have two dives I want to switch between, using a globle variable. 'Menu' page was visible at first but upon clicking I was supposed to see only the 'Game' div.
The variable inside the controller didn't upadte, but was only given the initial value of currentPage.
I decided to use the $window service inside the isPage function, but this didn't work either. Only when I called a function that tested the $window.currentPage variable, the pages switched - like I wanted:
JS:
var app = angular.module('someName', []);
var currentPage = 'Menu';
app.controller('PageController', ['$window','$scope', function($window,$scope){
this.isPage = function(page){
return (page == $window.currentPage);
};
this.button2onClick = function() {
$window.alert($window.currentPage);
}
}]);
function button1onClick(){
currentPage = 'Game';
}
HTML:
<button onclick="button1onClick()">CLICK ME</button> //Button 1
<div ng-controller="PageController">
<button ng-click="page.button2onClick">CLICK ME</button> //Button 2
<div id="Game" ng-show="page.isPage('Game')">
...
</div>
<div id="Menu" ng-show="page.isPage('Menu')">
...
</div>
</div>
So the only way I was able to update the pages is to call a function that tests the variable, thus updating the variable in AngularJS.
Is there a way to access a global variable without needing to test it to update it?
Am I doing something wrong? I don't want to convert my whole site to AngularJS-style, I like the code the way it is. Is AngularJS not the framework for me?
EDIT:
some things to clear out:
I'm new to AngularJS, so if you could explain what your answer does it would be great.
The whole reason why I do this instead of redirecting to another page is not to shut down socket.io 's connection
OP, take a look at UI Router.
var app = angular.module("app", ['ui.router']);
app.config(['$urlRouterProvider', '$stateProvider', function($urlRouterProvider, $stateProvider) {
$urlRouterProvider.otherwise('/main');
$stateProvider.state('main', {
controller: 'MainCtrl',
templateUrl: 'main.html',
url: '/main/'
}).state('game', {
controller: 'GameCtrl',
url: '/game/',
templateUrl: 'game.html'
});
}]);
HTML links:
<a ui-sref="main">Go to Main</a>
<a ui-sref="game">Go to Game</a>
View injection
<div ui-view="">
</div>
You should not use $window as a map object.
You should probably create a PageService:
angular.module('someName')
.factory('Page', [function(){
var currentPage = 'Menu';
return {
getPage: function() {
return currentPage;
},
isPage: function(page) {
return page === currentPage;
},
setPage: function(page) {
currentPage = page;
}
}
}]);
app.controller('PageController', ['Page','$scope', function(Page,$scope){
this.currentPage = Page.getPage();
this.isPage = Page.isPage;
this.button10Click = function(){
Page.setPage('Game');
}
}]);
HTML
<div class="button" ng-click="page.button10Click()">Game</div>
After reading malix's answer and KKKKKKKK's answer, and after researching a bit, I was able to solve my problem, and even write a better looking code.
To switch divs as I wanted in the example, I used ui-router, almost exactly the way KKKKKKKK did. The only difference is that I change state programmaticly - $state.go('menu')
To access global variables in other places in my code, I had to re-structure my whole code to fit AngularJS's structure, and used a Service, similarly to malix's answer:
app.factory('Data', function(){
var Data = {};
//DEFINE DATA
Data.stateChange = function(){};
Data.menuData = {};
Data.randomColors = ['#35cd96', '#6bcbef', '#E8E1DA', '#91ab01'];
/RETURN DATA
return Data;
});
It can be done using $rootScope. Variable initialize once can be accessible in other controllers.
function Ctrl1($scope, $rootScope) {
$rootScope.GlobalJSVariableA= window.GlobalJSVariable; }
Any controller & any view can access it now
function CtrlN($scope, $rootScope) {
$scope.GlobalJSVariableA= $rootScope.GlobalJSVariableA;
}
inside view I'm conditionally rendering html using ng-include and ng-if
<div ng-controller="myController">
<div ng-if="myProperty == 1">
<div ng-include="'view1.html'"></div>
</div>
<div ng-if="myProperty == 2">
<div ng-include="'view2.html'"></div>
</div>
</div>
and inside controller I have $scope.myProperty which receive value inside controller using $scope injection from other js object. On this controller I have also callback function which updates $scope.myProperty every x seconds.
app.controller('myController', function ($scope) {
...
$scope.myProperty = 0; //init value
function callback() {
$scope.$apply(); // force update view
// correctly write myProperty value on every data change
console.log($scope.myProperty);
}
var otherJsObject= new myObject($scope, callback);
otherJsObject.work();
...
}
callback function correctly change myProperty value but it doesn't update inside view every time.
update:
$scope.bindUIProp = { a: $scope.myProperty};
function callback() {
$scope.$apply();
$scope.bindUIProp.a = $scope.myProperty;
console.log('Callback ' + $scope.myProperty);
console.log('drawstage ' + $scope.bindUIProp.a);
}
var otherJsObject= new myObject($scope, callback);
otherJsObject.work();
and inside view I used object property
<div ng-controller="myController">
<div ng-if="bindUIProp.a == 1">
<div ng-include="'view1.html'"></div>
</div>
<div ng-if="bindUIProp.a == 2">
<div ng-include="'view2.html'"></div>
</div>
</div>
this approach work every time when page is refreshed, parial view is not updated from view1 to view2 when scope.bindUIProp.a is changed to 2.
Instead of writing to property at root level. Write one level below.
Instead of $scope.myProperty,
use $scope.mp.myProperty
Both ng-if and ng-include create child scopes.
You are having problems due to using a primitive in your main scope. Primitives don't have inheritance so the binding is getting broken in the nested child scopes.
change it to an object:
$scope.myProperty = { someProp: 0};
Personally I rarely use ng-include because of the child scope it creates. I prefer having my own directive if all I want is to include a template.
Very new to Angular and after searching all over the show I simply cannot find a solution to my problem.
I have the following function in a directive/controller:
ModalIssueController.prototype.openModal = function (e, issue) {
this._dataService.getMain().then(function (model) {
this._$scope.modalIssue.open = true;
this._$scope.modalIssue.issue = model.getIssueById(issue);
this._windowService.setModalOpen(true);
}.bind(this));
};
The above function is called each time the user clicks on a different issue from a list. This opens a modal and shows the content related to issue.
When the modal is closed via a close button, the following is called:
ModalIssueController.prototype.closeModal = function () {
this._$scope.modalIssue.open = false;
this._windowService.setModalOpen(false);
this._$timeout(function () {
this._$location.url('/');
}.bind(this));
};
The problem is, even though I can see that the value of this._$scope.modalIssue.issue changes to reflect the new issue that was clicked, the content in the modal never changes, but instead, continues to show the data from the first selected issue ;(
Am I missing something here? Is there an additional step I need to add to ensure that the data in the template is updated?
Here is the directive 'set-up':
var ModalIssueDirective = function () {
return {
restrict: 'A',
replace: true,
scope: true,
controller: ModalIssueController,
templateUrl: '/app/lorax/directives/modal-issue.tpl.html'
};
};
And here is the template I am populating:
<section class="modal modal--fade-show modal--issue" ng-show="modalIssue.open" >
Close
<h1 class="detail-header-title">{{::modalIssue.issue.getTitle()}}</h1>
<div class="detail-main__copy">{{::modalIssue.issue.getNarrative()}}</div>
<header class="detail-link__header">
<h1>{{::modalIssue.issue.getMiscLocale().mozDoingLabel}}</h1>
</header>
<p class="detail-link__copy">{{::modalIssue.issue.getMozActionCopy()}}</p>
<a ng-if="::modalIssue.issue.getMozActionLink().length === 1" href="{{::modalIssue.issue.getMozActionLink()[0].url}}" class="btn detail-link__btn">{{::modalIssue.issue.getMozActionLink()[0].copy}}</a>
<a ng-if="::modalIssue.issue.getMozActionLink().length > 1" ng-repeat="link in ::modalIssue.issue.getMozActionLink()" href="{{link.url}}" class="detail-link__multiple">{{link.copy}}<span class="icon-arrow-right"></span></a>
<header class="detail-link__header">
<h1>{{::modalIssue.issue.getMiscLocale().yourDoingLabel}}</h1>
</header>
<p class="detail-link__copy">{{::modalIssue.issue.getYourActionCopy()}}</p>
<a ng-if="::modalIssue.issue.getYourActionLink().length === 1" href="{{::modalIssue.issue.getYourActionLink()[0].url}}" class="btn detail-link__btn">{{::modalIssue.issue.getYourActionLink()[0].copy}}</a>
<a ng-if="::modalIssue.issue.getYourActionLink().length > 1" ng-repeat="link in ::modalIssue.issue.getYourActionLink()" href="{{link.url}}" class="detail-link__multiple">{{link.copy}}<span class="icon-arrow-right"></span></a>
</section>
Thank you in advance for any assistance that can be provided here.
So, turns out :: in Angular templates defines a one-time binding. This essentially means that as soon as, for example, the following expression has been run:
{{::modalIssue.issue.getTitle()}}
and it returned a value that is not undefined, it is considered stable and the expression will never be run again. So, removing :: from each of the relevant lines in the template resolved the issue.
Docs: https://docs.angularjs.org/guide/expression (#see One-Time Binding)