The app has a controller, that uses a service to create an instance of video player. The video player triggers events to show progress every few seconds. When the video reaches to a certain point, I want to show a widget on top of the video player.
The view has the widget wrapped in ng-show directive.
It takes more then 60 seconds for the dom element to receive the signal to remove the ng-hide class after the event has been triggered and the values have been populated.
If I try to implement this using the plain dom menthod (like document.getElementById(eleId).innerHTML = newHTML), the update is instant.
What am I doing wrong? Here is the complete sequence in code:
Controller:
MyApp.controller('SectionController', ['$scope', 'PlayerService'], function($scope, PlayerService){
$scope.createPlayer = function() {
PlayerService.createPlayer($scope, wrapperId);
}});
Service:
MyApp.service('PlayerService', [], function(){
this.createPlayer=function(controllerScope, playerWrapper){
PLAYER_SCRIPT.create(playerWrapper) {
wrapper : playerWrapper,
otherParam : value,
onCreate : function(player) {
player.subscribe(PLAY_TIME_CHANGE, function(duration){
showWidget(controllerScope, duration);
})
}
}
}
function showWidget(controllerScope, duration) {
if(duration>CERTAIN_TIME) {
$rootScope.widgetData = {some:data}
$rootScope.showWidget = true;
}
}});
View:
<div ng-show="showWidget"> <div class="wdgt">{{widgetData.stuff}}</div> </div>
Solved it! $scope.$apply() did the trick.
My guess is, due to other complex logic ad bindings inside the app, there was a delay in computing the change by angular the default way.
#floribon Thanks for the subtle hint about "complex angular stuff".
The code inside the service function changed to:
function showWidget(controllerScope, duration) {
if(duration>CERTAIN_TIME) {
$rootScope.widgetData = {some:data}
$rootScope.showWidget = true;
$rootScope.$apply();
}}
Do you have complex angular stuff within your hidden view?
You should try to use ng-if instead of ng-show, the difference being that when the condition is false, ng-if will remove the element from the DOM instead of just hidding it (which is also what you do in vanilla JS).
When the view is simply hidden using ng-show however, all the watchers and bindings within it keep being computed by Angular. Let us know if ng-if solve your problem, otherwise I'll edit my answer.
Related
I'm new in angular and i'm looking for the best way to do what I want.
In my main page I have 2 directives, one is used to display a button (and maybe other stuff). And another used to display a kind of dialog box/menu.
Each directive has its own controller.
I want to show or hide the second directive when I click on the button in the first one.
I don't really know what are goods or wrong approaches. Should I use a service injected in both controller and set a variable with ng-show in the second directive? This solution doesn't really hide the directive because I need a div inside the directive to hide its content and isn't too much to use a service only for one boolean?
Should I use a kind of global variable (rootscope?) or inject the first controller inside the second one?
Or maybe use a third controller in my main page (used with a service?) or use only one controller for both directive?
Basically without directive I would probably used only one main controller for my whole page and set a variable.
In fact the first directive is just a kind of button used to display "something", and the second directive just a kind of popup waiting a boolean to be displayed. That's why I finally used a service containing a boolean with a getter and a setter to avoid any interaction beetween both controller.
My both controller use this service, the first one to set the value when we click on the element and the second controller provide just a visibility on the getter for my ng-show.
I don't know if it is the best way to do but I am satisfied for now.
Small example here (without directive but with same logic) :
http://codepen.io/dufaux/pen/dXMrPm
angular.module('myModule', []);
angular.module("myModule")
.controller("ButtonCtrl", buttonCtrl)
.controller("PopUpCtrl", popUpCtrl)
.service("DisplayerService", displayerService);
//ButtonCtrl
buttonCtrl.$inject = ["DisplayerService", "$scope"];
function buttonCtrl(DisplayerService, $scope) {
var vm = this;
vm.display = function(){
DisplayerService.setDisplay(!DisplayerService.getDisplay());
}
}
//PopUpCtrl
popUpCtrl.$inject = ["DisplayerService"];
function popUpCtrl(DisplayerService) {
var vm = this;
vm.displayable = function(){
return DisplayerService.getDisplay();
}
}
//Service
function displayerService(){
var vm = this;
vm.display = false;
vm.setDisplay = function(value){
vm.display = value;
}
vm.getDisplay = function(){
return vm.display;
}
}
--
<body data-ng-app="myModule">
<div data-ng-controller="ButtonCtrl as btnCtrl" >
<button data-ng-click="btnCtrl.display()">
display
</button>
</div>
[...]
<div data-ng-controller="PopUpCtrl as popUpCtrl" >
<div data-ng-show="popUpCtrl.displayable()">
hello world
</div>
</div>
</body>
Scenario...
I'm building a date picker directive and untill now I managed to keep strict separation between template/controller concerns. The controller builds a month array full of day objects. The template uses ng-repeat="day in days" to render current month dates.
Currently I am building an interval selection method which is invoked by: ng-mousedown="startSelection(date.dayId);". While the click is pressed a range of dates is defined in the model as the selected dates, and highlighted in the template via a css class. On mouse button release the range is defined one last time in order to obtain the final date selection.
This works fine, however there is a small issue that needs to be tackled. While the mouse is pressed and startSelection does it's job also the browser highlights the text in the specific blue shade. I want to prevent this behavior by attaching a no-select css class to the body element while the mouse is pressed, and then removing it when selection sequence has ended.
(I choose the body element because if the cursor slides of the directive into the app body, text will be selected from everywhere in the page.)
.no-select {
-webkit-touch-callout:none;
-webkit-user-select:none;
-khtml-user-select:none;
-moz-user-select:none;
-ms-user-select:none;
user-select:none;
}
... and Question
And here's what troubles me: Since we are not supposed to manipulate DOM from the controller (especially external elements) in order to facilitate unit testing, what is the recommended way to do this operation? Are there any guidelines/best practices for fringe cases like this one? Is it overkill to create a service dedicated for text selection prevention? I guess that would best fit as a method into an utils service.
Directive controller:
app.directive("caDatePicker", function () {
return {
...
controller: function ($scope) {
...
$scope.startSelection = function (dayId) {
angular.element(document.querySelector('body')).addClass('no-select');
...
};
...
}
}
}
Slightly different scenario:
What if I add a class on the directive element <ca-date-picker></ca-date-picker> like in the following code sample. Is this ok practice? Does this infringe the controller-template separation of concerns guideline?
link: function (scope, element, attrs) {
element.addClass("no-select");
...
}
You could set a boolean in the controller and then in your directive you could watch a change on this model attribute and do your dom manipulation in the directive :
controller: function ($scope) {
$scope.dayId = "";
$scope.selectionStarted = false;
$scope.startSelection = function (dayId) {
$scope.dayId = dayId;
$scope.selectionStarted = true;
};
...
}
then in your directive :
$scope.$watch('selectionStarted', function(newValue, oldValue) {
if(newValue) {
angular.element(document.querySelector('body')).addClass('no-select');
}
});
Basically take this as a rule of thumb. Never ever manipulate your dom in your controllers ever.
Your controller is the glue that controls the behaviour of your view and the directive is what depending on the status of your controller $scope variables is what manipulates the dom.
if your element that you want to manipulate is outside of your direcitve scope or controller scope. but an attribute of that controller is responsible for if or not the dom manipulation should be applied. You can do this :
in your template :
ng-class="noSelectBoolean ? 'no-select-css-class' : 'select-css-class'"
in your controller that is in scope of your template :
.controller(function($scope) {
$scope.noSelectBoolean = false;
$scope.$on('applyNoSelect', function(event, value) {
$scope.noSelectBoolean = value;
}
}
and lastly in your controller that decides if it should be applied or not :
.controller(function($scope, $rootScope){
$scope.noSelect = function(value){
$rootScope.$broadcast('applyNoSelect', value);
}
}
for more a more indepth explaination check out a previous answer I submitted :
Event broadcasting in angularJS
I have an AngularJS Application with a scroll directive implemented as the following:
http://jsfiddle.net/un6r4wts/
app = angular.module('myApp', []);
app.run(function ($rootScope) {
$rootScope.var1 = 'Var1';
$rootScope.var2 = function () { return Math.random(); };
});
app.directive("scroll", function ($window) {
return function(scope, element, attrs) {
angular.element($window).bind("scroll", function() {
if (this.pageYOffset >= 100) {
scope.scrolled = true;
} else {
scope.scrolled = false;
}
scope.$apply();
});
};
});
The HTML looks the following:
<div ng-app="myApp" scroll ng-class="{scrolled:scrolled}">
<header></header>
<section>
<div class="vars">
{{var1}}<br/><br/>
{{var2()}}
</div>
</section>
</div>
I only want the class scrolled to be added to the div once the page is scrolled more than 100px. Which is working just fine, but I only want that to happen! I don't want the whole scope to be re-rendered. So the function var2() should not be executed while scrolling. Unfortunately it is though.
Is there any way to have angular only execute the function which is bound to the window element without re-rendering the whole scope, or am I misunderstanding here something fundamentally to AngularJS?
See this fiddle:
http://jsfiddle.net/un6r4wts/
Edit:
This seems to be a topic about a similar problem:
Angularjs scope.$apply in directive's on scroll listener
If you want to calculate an expression only once, you can prefix it with '::', which does exactly that. See it in docs under One-time binding:
https://docs.angularjs.org/guide/expression
Note, this requires angular 1.3+.
The reason that the expressions are calculated is because when you change a property value on your scope, then dirty check starts and evaluates all the watches for dirty check. When the view uses {{ }} on some scope variable, it creates a binding (which comes along with a watch).
Inside of my directive I have this and it works only in the beginning, and whenever I resize later it never fires.
scope.$watch ->
cw: element[0].clientWidth
,(newValue, oldValue)->
if newValue.cw isnt oldValue.cw
console.log "changed"
,true
Your function looks right to me, but it's important to note that it won't fire on resize unless you manually trigger a digest on the resize event.
Without this, angular doesn't realise there's been an event that should trigger a digest, and so none happen.
Try adding this (remember to inject $window):
angular.element($window).bind('resize', ()->
scope.$apply();
)
Just like Jason Goemaat said, its because Watches are only checked on a $digest cycle
Depending on how the way your element is resized maybe this example may help you.
Here is a menu that expands it self when pressed on the arrow.
<div class="menu" ng-class="{'expanded' : main.expanded}">
<div class="menu-handler" ng-click="main.expanded = !main.expanded">
</div>
</div>
The menu is expanded by a ng-click that makes a change on the scope, in this case its a boolean var that acts as a flag. As the opening of the menu is made throught the change in the scope it calls the $digest cycle iterating through all $watch.
scope.$watch(function() {
return element[0].clientWidth;
}, function(oldValue, newValue) {
console.log(oldValue, newValue);
});
You can view the full example in this JSfiddle
https://jsfiddle.net/owenbcal/zf17s0mL/
Hope i was helpfull.
the way I know about watching this kind of values is making a function that is fired once the value changes so then I watch that function :
in the service:
this.getDataOut = function () {
return self.dataOut;
};
in the controller:
$scope.$watch(dataService.getDataOut, function() {
$scope.dataOut = dataService.dataOut;
});
I've built a simple directive that adds a javascript-based loading animation. It is operating with a window.setInterval() loop. This works great, but when loading is complete, I use ngSwitch to swap in my content, which removes the element housing the loading directive attribute from the page.
Ideally, I'd like to watch for this change and clear my interval so the animation calculations are not still running in the background. I have tried watching a custom function that evaluates the presence of the element on the page. I know the function works at detecting this, but it seems timing is an issue -- namely, as far as I can tell, the $watch itself is cleared when the directive attribute's element leaves the page. My $watch'ed expression therefore never detects a change and never calls its callback that clears the animation interval function.
Is there a recommended pattern for dealing with this type of situation?
Relevant snippet from my template:
<div ng-switch on="dataStatus">
<div ng-switch-when="loading">
<div loading-spinner></div>
</div>
<div ng-switch-when="haveData">
<!-- data dependent on content we were loading -->
</div>
</div>
Simplified version of my directive:
myModule.directive('loadingSpinner', function () {
var updateMySweetAnimation = function (element) { /* ... */ };
return {
link: function (scope, iElement, iAttrs) {
var spinner = window.setInterval(function () {
updateMySweetAnimation(iElement);
}, 100);
scope.$watch(function () {
return $(document).find(iElement).length;
}, function (present) {
if (!present) {
clearInterval(spinner);
}
});
}
};
});
When the element is cleared from the page by ng-switch, two things should happen:
The scope created for ng-switch-when, the element with your directive on, is destroyed. This kills your $watch and generates a $destroy event across the scope that you can watch with scope.$on('$destroy', ...).
The element is removed from the DOM. This generates a separate destroy event that you can watch with iElement.on('$destroy', ...).
They should happen in this order, looking at the latest stable release (1.0.8 - https://github.com/angular/angular.js/blob/v1.0.8/src/ng/directive/ngSwitch.js), so your scope and thus your watch should always be dead when the element is removed from the DOM.
You could avoid this problem by watching from the outer scope, where ng-switch is defined. Or you could watch dataStatus, the same condition as in your ng-switch, rather than looking for the results of the ng-switch seeing your condition change.
Both of these would probably work, but actually all you need to do, and in fact the normal pattern for this, is to just watch for one of the $destroy events and clean everything up there. As the interval feels more relevant to the view than the model, I would use the DOM event and replace your $watch with
iElement.on('$destroy', function(){
clearInterval(spinner);
});