I am trying to use AngularJS to detect bootstrap environment. This is my code:
angular.module("envService",[])
.factory("envService", envService);
function envService($window){
return env();
////////////
function env(){
var w = angular.element($window);
var winWidth = w.width();
if(winWidth<768){
return 'xs';
}else if(winWidth>=1200){
return 'lg';
}else if(winWidth>=992){
return 'md';
}else if(winWidth>=768){
return 'sm';
}
}
}
The function works and return the value based on the window size. However, it will always return the same environment even if the window size is changed. How can I fix it?
You need to watch for the window resize event.
angular.module('envService',[])
.factory('envFactory', ['$window', '$timeout', function($window, $timeout) {
var envFactory = {};
var t;
envFactory.getEnv = function () {
var w = angular.element($window);
var winWidth = w.width();
if(winWidth<768){
return 'xs';
}else if(winWidth>=1200){
return 'lg';
}else if(winWidth>=992){
return 'md';
}else if(winWidth>=768){
return 'sm';
}
};
angular.element($window).bind('resize', function () {
$timeout.cancel(t);
t = $timeout(function () {
return envFactory.getEnv();
}, 300); // check if resize event is still happening
});
return envFactory;
}]);
angular.module('app',['envService']).controller('AppController', ['$scope', 'envFactory',
function($scope, envFactory) {
// watch for changes
$scope.$watch(function () { return envFactory.getEnv() }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.env = newVal;
console.log($scope.env);
}
});
}
]);
Related
I'm struggling creating a directive to assign and update a variable, that compares to the window width, and updates with resize.
I need the variable as compared to using CSS because I will work it into ng-if. What am I doing wrong? Here is the code:
var app = angular.module('miniapp', []);
function AppController($scope) {}
app.directive('widthCheck', function ($window) {
return function (scope, element, attr) {
var w = angular.element($window);
scope.$watch(function () {
return {
'w': window.innerWidth
};
}, function (newValue, oldValue, desktopPlus, isMobile) {
scope.windowWidth = newValue.w;
scope.desktopPlus = false;
scope.isMobile = false;
scope.widthCheck = function (windowWidth, desktopPlus) {
if (windowWidth > 1399) {
scope.desktopPlus = true;
}
else if (windowWidth < 769) {
scope.isMobile = true;
}
else {
scope.desktopPlus = false;
scope.isMoblie = false;
}
}
}, true);
w.bind('resize', function () {
scope.$apply();
});
}
});
JSfiddle here: http://jsfiddle.net/h8m4eaem/2/
As mentioned in this SO answer it's probably better to bind to the window resize event with-out watch. (Similar to Mr. Berg's answer.)
Something like in the demo below or in this fiddle should work.
var app = angular.module('miniapp', []);
function AppController($scope) {}
app.directive('widthCheck', function($window) {
return function(scope, element, attr) {
var width, detectFalse = {
desktopPlus: false,
isTablet: false,
isMobile: false
};
scope.windowWidth = $window.innerWidth;
checkSize(scope.windowWidth); // first run
//scope.desktopPlus = false;
//scope.isMoblie = false; // typo
//scope.isTablet = false;
//scope.isMobile = false;
function resetDetection() {
return angular.copy(detectFalse);
}
function checkSize(windowWidth) {
scope.detection = resetDetection();
if (windowWidth > 1000) { //1399) {
scope.detection.desktopPlus = true;
} else if (windowWidth > 600) {
scope.detection.isTablet = true;
} else {
scope.detection.isMobile = true;
}
}
angular.element($window).bind('resize', function() {
width = $window.innerWidth;
scope.windowWidth = width
checkSize(width);
// manuall $digest required as resize event
// is outside of angular
scope.$digest();
});
}
});
.fess {
border: 1px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="miniapp" ng-controller="AppController">
<div width-check class="fess" resize>
window.width: {{windowWidth}}
<br />desktop plus: {{detection.desktopPlus}}
<br />mobile: {{detection.isMobile}}
<br />tablet: {{detection.isTablet}}
<br/>
</div>
</div>
scope.widthCheck is assigned an anonymous function and RE-ASSIGNED that same function each time this watcher fires. I also notice it's never called.
you should move that piece out of the $watch and call the function when the watcher fires
There's no need for a watch, you're already binding to resize. Just move your logic in there. And as Jun Duan said you continously create the funciton. Here's the change:
app.directive('widthCheck', function ($window) {
return function (scope, element, attr) {
function widthCheck(windowWidth) {
if (windowWidth > 1399) {
scope.desktopPlus = true;
}
else if (windowWidth < 769) {
scope.isMobile = true;
}
else {
scope.desktopPlus = false;
scope.isMoblie = false;
}
}
windowSizeChanged();
angular.element($window).bind('resize', function () {
windowSizeChanged();
scope.$apply();
});
function windowSizeChanged(){
scope.windowWidth = $window.innerWidth;
scope.desktopPlus = false;
scope.isMobile = false;
widthCheck(scope.windowWidth);
}
}
});
jsFiddle: http://jsfiddle.net/eqd3zu0j/
In angular if I register watch events dynamically (in my case in a for loop), the watch does not work. Please take a look at a fiddle. Any ideas?
var myApp = angular.module('myApp',[]);
function MyCtrl($scope, $parse) {
// 1. binding watch inside loop (doesn't work):
$scope.aaa = 1;
$scope.bbb = 2;
$scope.dependsOn = [
function () { return $scope.aaa; },
function () { return $scope.bbb; }
];
for (var i = 0; i < $scope.dependsOn.length; i++) {
$scope.$watch(
function () {
return $scope.dependsOn[i];
},
function (newVal) {
if (newVal !== undefined) {
console.log("doesn't work");
}
}
);
}
$scope.aaa = 5;
$scope.bbb = 6;
// binding watch not inside loop (works):
$scope.ccc = 1;
$scope.ddd = 2;
$scope.dependsOn = [
function () { return $scope.ccc; },
function () { return $scope.ddd; }
];
$scope.$watch(
function () {
return $scope.dependsOn[0];
},
function (newVal) {
if (newVal !== undefined) {
console.log("works");
}
}
);
$scope.$watch(
function () {
return $scope.dependsOn[1];
},
function (newVal) {
if (newVal !== undefined) {
console.log("works");
}
}
);
$scope.ccc = 5;
$scope.ddd = 6;
}
fiddle
The problem you are experiencing is because you are capturing the variable i in a closure. Then i gets incremented to the value 2 and drops out of the loop, each of your delegates will have 2 as their i value upon actually executing the delegate.
Demonstrates:
http://jsfiddle.net/df6L0v8f/1/
(Adds:)
$scope.$watch(
function () {
console.log(i);
return $scope.dependsOn[i];
},
function (newVal) {
if (newVal !== undefined) {
console.log("doesn't work");
}
}
);
You can fix this and the issue of variable hoisting by using a self calling closure to capture the value at the time of iteration and maintain that for your delegate.
http://jsfiddle.net/df6L0v8f/4/
for (var i = 0; i < $scope.dependsOn.length; i++) {
var watchDelegate = (function(itemDelegate){
return function () {
return itemDelegate;
};
})($scope.dependsOn[i]);
$scope.$watch(
watchDelegate,
function (newVal) {
if (newVal !== undefined) {
console.log(newVal());
}
}
);
}
First I want to say that I am a complete beginner in AngularJS and just attempting to understand the basic concepts. I have a background in Java and PHP.
I am building a part of a website. Right now the angular app only consists of opening and closing 2 drop down menus registrationDropDown and loginDropDown. I want them to work so that only one can be open at a time ie. if I open one, and the other is already open, the older one is forced to close.
I have a service to manage the variables that determine whether the drop downs should be open or closed and 2 controllers, one for login and one for registration, both include $watch for the respective variables.
THE PROBLEM
I want the app to work so that only one of the drop downs can be open at one time.
JSFIDDLE: http://jsfiddle.net/F5p6m/3/
angular.module("ftApp", [])
.factory('dropDownService', function () {
var loginDropDownStatus = false;
var registrationDropDownStatus = false;
return {
getLoginDropDownStatus: function () {
return loginDropDownStatus;
},
showLoginDropDown: function () {
console.log("showing login drop down");
registrationDropDownStatus = false;
loginDropDownStatus = true;
console.log("loginDropDownStatus" + loginDropDownStatus + "registrationDropDownStatus" + registrationDropDownStatus);
},
hideLoginDropDown: function () {
console.log("hiding login drop down");
loginDropDownStatus = false;
console.log("loginDropDownStatus" + loginDropDownStatus);
},
getRegistrationDropDownStatus: function () {
return registrationDropDownStatus;
},
showRegistrationDropDown: function () {
console.log("showing registration drop down");
registrationDropDownStatus = true;
loginDropDownStatus = false;
console.log("registrationDropDownStatus" + registrationDropDownStatus);
},
hideRegistrationDropDown: function () {
console.log("hiding registration drop down");
registrationDropDownStatus = false;
console.log("registrationDropDownStatus" + registrationDropDownStatus);
}
};
}) .controller("LoginDropDownController", function ($scope, dropDownService) {
$scope.loginDropDownStatus = dropDownService.getLoginDropDownStatus();
$scope.$watchCollection('loginDropDownStatus', function(newValue, oldValue) {
console.log("watcher is working");
console.log("value is " + newValue + oldValue);
console.log("LOGIN new value is " + newValue);
$scope.loginDropDownStatus = newValue;
});
$scope.toggleDropDown = function () {
if ( $scope.loginDropDownStatus == false ) {
dropDownService.showLoginDropDown();
dropDownService.hideRegistrationDropDown();
$scope.loginDropDownStatus = true;
} else if ( $scope.loginDropDownStatus == true ) {
dropDownService.hideLoginDropDown();
$scope.loginDropDownStatus = false;
}
};
})
.controller("RegistrationDropDownController", function ($scope, dropDownService) {
$scope.registrationDropDownStatus = dropDownService.getRegistrationDropDownStatus();
$scope.$watch('registrationDropDownStatus', function(newValue, oldValue) {
console.log("watcher is working");
console.log("value is " + newValue + oldValue);
console.log("new value is " + newValue);
$scope.registrationDropDownStatus = newValue;
});
$scope.toggleDropDown = function () {
if ( $scope.registrationDropDownStatus == false ) {
dropDownService.showRegistrationDropDown();
dropDownService.hideLoginDropDown();
$scope.registrationDropDownStatus = true;
} else if ( $scope.registrationDropDownStatus == true ) {
dropDownService.hideRegistrationDropDown();
$scope.registrationDropDownStatus = false;
}
};
})
Edit:
Here is probably the shortest option:
angular.module("ftApp", [])
.controller("ctrl", function ($scope) {
$scope.toggle = function(menu){
$scope.active = $scope.active === menu ? null : menu;
}
})
FIDDLE
One controller, no service.
Previous Answer:
I think you have quite a bit of code to get something very simple done. Here is my solution:
angular.module("ftApp", [])
.service('dropDownService', function () {
this.active = null;
this.toggle = function(menu){
this.active = this.active === menu ? null : menu;
}
})
.controller("LoginDropDownController", function ($scope, dropDownService) {
$scope.status = dropDownService;
$scope.toggleDropDown = function () {
dropDownService.toggle("login");
};
})
.controller("RegistrationDropDownController", function ($scope, dropDownService) {
$scope.status = dropDownService;
$scope.toggleDropDown = function () {
dropDownService.toggle("reg");
};
})
FIDDLE
You can make it even shorter by only using one controller. You wouldn't even need the service then.
You are overcomplicating things. All you need your service to hold is a property indicating which dorpdown should be active.
Then you can change that property's value from the controller and check the value in the view to determine if a dropdown should be shown or hidden.
Something like this:
<!-- In the VIEW -->
<li ng-controller="XyzController">
<a ng-click="toggleDropdown()">Xyz</a>
<div ng-show="isActive()">Dropdown</div>
</li>
/* In the SERVICE */
.factory('DropdownService', function () {
return {
activeDropDown: null
};
})
/* In the CONTROLLER */
controller("XyzDropdownController", function ($scope, DropdownService) {
var dropdownName = 'xyz';
var dds = DropdownService;
$scope.isActive = function () {
return dropdownName === dds.activeDropdown;
};
$scope.toggleDropdown = function () {
dds.activeDropdown = (dds.activeDropdown === dropdownName) ?
null :
dropdownName;
};
})
See, also, this short demo.
Based on what exactly you are doing, there might be other approaches possible/preferrable:
E.g. you could use just on controller to control all dropdowns
or you could use two instances of the same controller to control each dropdown.
See my updated fiddle. I simplified the code and removed the service. Because you just used two variables to control visibility, you don't need a service nor $watch. You need to keep variables in the $rootScope, otherwise changes in a controller is not visible to another controller due to isolated scopes.
angular.module("ftApp", [])
.controller("LoginDropDownController", function ($scope, $rootScope) {
$rootScope.loginDropDownStatus = false;
$scope.toggleDropDown = function () {
if ($rootScope.loginDropDownStatus == false) {
$rootScope.registrationDropDownStatus = false;
$rootScope.loginDropDownStatus = true;
} else if ($rootScope.loginDropDownStatus == true) {
$rootScope.loginDropDownStatus = false;
}
};
}).controller("RegistrationDropDownController", function ($scope, $rootScope) {
$rootScope.registrationDropDownStatus = false;
$scope.toggleDropDown = function () {
if ($rootScope.registrationDropDownStatus === false) {
$rootScope.loginDropDownStatus = false;
$rootScope.registrationDropDownStatus = true;
} else if ($scope.registrationDropDownStatus === true) {
$rootScope.registrationDropDownStatus = false;
}
};
})
This code can be simplified further. I'll leave that to you.
My goal is to autosave a form after is valid and update it with timeout.
I set up like:
(function(window, angular, undefined) {
'use strict';
angular.module('nodblog.api.article', ['restangular'])
.config(function (RestangularProvider) {
RestangularProvider.setBaseUrl('/api');
RestangularProvider.setRestangularFields({
id: "_id"
});
RestangularProvider.setRequestInterceptor(function(elem, operation, what) {
if (operation === 'put') {
elem._id = undefined;
return elem;
}
return elem;
});
})
.provider('Article', function() {
this.$get = function(Restangular) {
function ngArticle() {};
ngArticle.prototype.articles = Restangular.all('articles');
ngArticle.prototype.one = function(id) {
return Restangular.one('articles', id).get();
};
ngArticle.prototype.all = function() {
return this.articles.getList();
};
ngArticle.prototype.store = function(data) {
return this.articles.post(data);
};
ngArticle.prototype.copy = function(original) {
return Restangular.copy(original);
};
return new ngArticle;
}
})
})(window, angular);
angular.module('nodblog',['nodblog.route'])
.directive("autosaveForm", function($timeout,Article) {
return {
restrict: "A",
link: function (scope, element, attrs) {
var id = null;
scope.$watch('form.$valid', function(validity) {
if(validity){
Article.store(scope.article).then(
function(data) {
scope.article = Article.copy(data);
_autosave();
},
function error(reason) {
throw new Error(reason);
}
);
}
})
function _autosave(){
scope.article.put().then(
function() {
$timeout(_autosave, 5000);
},
function error(reason) {
throw new Error(reason);
}
);
}
}
}
})
.controller('CreateCtrl', function ($scope,$location,Article) {
$scope.article = {};
$scope.save = function(){
if(typeof $scope.article.put === 'function'){
$scope.article.put().then(function() {
return $location.path('/blog');
});
}
else{
Article.store($scope.article).then(
function(data) {
return $location.path('/blog');
},
function error(reason) {
throw new Error(reason);
}
);
}
};
})
I'm wondering if there is a best way.
Looking at the code I can see is that the $watch will not be re-fired if current input is valid and the user changes anything that is valid too. This is because watch functions are only executed if the value has changed.
You should also check the dirty state of the form and reset it when the form data has been persisted otherwise you'll get an endless persist loop.
And your not clearing any previous timeouts.
And the current code will save invalid data if a current timeout is in progress.
I've plunked a directive which does this all and has better SOC so it can be reused. Just provide it a callback expression and you're good to go.
See it in action in this plunker.
Demo Controller
myApp.controller('MyController', function($scope) {
$scope.form = {
state: {},
data: {}
};
$scope.saveForm = function() {
console.log('Saving form data ...', $scope.form.data);
};
});
Demo Html
<div ng-controller="MyController">
<form name="form.state" auto-save-form="saveForm()">
<div>
<label>Numbers only</label>
<input name="text"
ng-model="form.data.text"
ng-pattern="/^\d+$/"/>
</div>
<span ng-if="form.state.$dirty && form.state.$valid">Updating ...</span>
</form>
</div>
Directive
myApp.directive('autoSaveForm', function($timeout) {
return {
require: ['^form'],
link: function($scope, $element, $attrs, $ctrls) {
var $formCtrl = $ctrls[0];
var savePromise = null;
var expression = $attrs.autoSaveForm || 'true';
$scope.$watch(function() {
if($formCtrl.$valid && $formCtrl.$dirty) {
if(savePromise) {
$timeout.cancel(savePromise);
}
savePromise = $timeout(function() {
savePromise = null;
// Still valid?
if($formCtrl.$valid) {
if($scope.$eval(expression) !== false) {
console.log('Form data persisted -- setting prestine flag');
$formCtrl.$setPristine();
}
}
}, 500);
}
});
}
};
});
UPDATE:
to stopping timeout
all the logic in the directive
.directive("autosaveForm", function($timeout,$location,Post) {
var promise;
return {
restrict: "A",
controller:function($scope){
$scope.post = {};
$scope.save = function(){
console.log(promise);
$timeout.cancel(promise);
if(typeof $scope.post.put === 'function'){
$scope.post.put().then(function() {
return $location.path('/post');
});
}
else{
Post.store($scope.post).then(
function(data) {
return $location.path('/post');
},
function error(reason) {
throw new Error(reason);
}
);
}
};
},
link: function (scope, element, attrs) {
scope.$watch('form.$valid', function(validity) {
element.find('#status').removeClass('btn-success');
element.find('#status').addClass('btn-danger');
if(validity){
Post.store(scope.post).then(
function(data) {
element.find('#status').removeClass('btn-danger');
element.find('#status').addClass('btn-success');
scope.post = Post.copy(data);
_autosave();
},
function error(reason) {
throw new Error(reason);
}
);
}
})
function _autosave(){
scope.post.put().then(
function() {
promise = $timeout(_autosave, 2000);
},
function error(reason) {
throw new Error(reason);
}
);
}
}
}
})
Here's a variation of Null's directive, created because I started seeing "Infinite $digest Loop" errors. (I suspect something changed in Angular where cancelling/creating a $timeout() now triggers a digest.)
This variation uses a proper $watch expression - watching for the form to be dirty and valid - and then calls $setPristine() earlier so the watch will re-fire if the form transitions to dirty again. We then use an $interval to wait for a pause in those dirty notifications before saving the form.
app.directive('autoSaveForm', function ($log, $interval) {
return {
require: ['^form'],
link: function (scope, element, attrs, controllers) {
var $formCtrl = controllers[0];
var autoSaveExpression = attrs.autoSaveForm;
if (!autoSaveExpression) {
$log.error('autoSaveForm missing parameter');
}
var savePromise = null;
var formModified;
scope.$on('$destroy', function () {
$interval.cancel(savePromise);
});
scope.$watch(function () {
// note: formCtrl.$valid is undefined when this first runs, so we use !$formCtrl.$invalid instead
return !$formCtrl.$invalid && $formCtrl.$dirty;
}, function (newValue, oldVaue, scope) {
if (!newValue) {
// ignore, it's not "valid and dirty"
return;
}
// Mark pristine here - so we get notified again if the form is further changed, which would make it dirty again
$formCtrl.$setPristine();
if (savePromise) {
// yikes, note we've had more activity - which we interpret as ongoing changes to the form.
formModified = true;
return;
}
// initialize - for the new interval timer we're about to create, we haven't yet re-dirtied the form
formModified = false;
savePromise = $interval(function () {
if (formModified) {
// darn - we've got to wait another period for things to quiet down before we can save
formModified = false;
return;
}
$interval.cancel(savePromise);
savePromise = null;
// Still valid?
if ($formCtrl.$valid) {
$formCtrl.$saving = true;
$log.info('Form data persisting');
var autoSavePromise = scope.$eval(autoSaveExpression);
if (!autoSavePromise || !autoSavePromise.finally) {
$log.error('autoSaveForm not returning a promise');
}
autoSavePromise
.finally(function () {
$log.info('Form data persisted');
$formCtrl.$saving = undefined;
});
}
}, 500);
});
}
};
});
Why is readingController not able to use the Romanize service? It always says Romanize is undefined inside the function. How can I get the service in scope?
var readingController = function (scope, Romanize){
scope.currentMaterial = scope.sections[scope.sectionNumber].tutorials[scope.tutorialNumber].material;
Romanize;
}
var app = angular.module('Tutorials', ['functions', 'tutorials']).controller('getAnswers', function ($scope, $element) {
$scope.sectionNumber = 0;
$scope.tutorialNumber = 0;
$scope.questionNumber = 0;
$scope.sections = sections;
$scope.loadFromMenu = function (sec, tut, first) {
if (tut === $scope.tutorialNumber && sec === $scope.sectionNumber && !first) {//if clicked on already playing tut
return;
}
if (tut !== undefined && sec !== undefined) {
$scope.tutorialNumber = tut;
$scope.sectionNumber = sec;
}
for (var x in sections) {
sections[x].active = "inactive";
for (var y in sections[x].tutorials){
sections[x].tutorials[y].active = "inactive";
}
}
var section = sections[$scope.sectionNumber];
section.active = "active";
section.tutorials[$scope.tutorialNumber].active = "active";
$scope.questionNumber = 0;
$scope.currentTutorialName = sections[$scope.sectionNumber].tutorials[$scope.tutorialNumber].name;
$scope.$apply();
if ($scope.sectionNumber === 0){
readingController($scope, app.Romanize);
}else if ($scope.sectionNumber === 1){
conjugationController($scope);
}
};
$scope.loadFromMenu(0,0, true);
var conjugationController = function (){
var loadNewVerbs = function (scope) {
scope.currentVerbSet = scope.sections[scope.sectionNumber].tutorials[scope.tutorialNumber].verbs;
if (scope.currentVerbSet === undefined) {
alert("Out of new questions");
return
}
scope.verbs = conjugate(scope.currentVerbSet[scope.questionNumber]);
scope.correct = scope.verbs.conjugations[0].text;
fisherYates(scope.verbs.conjugations);
scope.$apply();
};
loadNewVerbs($scope);
$scope.checkAnswer = function (answer) {
if($scope.sectionNumber === 0 && $scope.tutorialNumber === 0 && $("video")[0].currentTime < 160){
$scope.message = "Not yet!";
$(".message").show(300).delay(900).hide(300);
return;
}
answer.colorReveal = "reveal-color";
if (answer.text === $scope.correct) { //if correct skip to congratulations
$scope.questionNumber++;
setTimeout(function () {
loadNewVerbs($scope);
$scope.$apply();
}, 2000);
} else { //if incorrect skip to try again msg
if ($scope.sectionNumber === 0 && $scope.tutorialNumber === 0) {
start(160.5);
pause(163.8)
}
}
};
};
});
app.factory('Romanize', ['$http', function($http){
return{
get: function(){
$http.get(scope.sections[scope.sectionNumber].romanizeService).success(function(data) {
$scope.romanized = data;
});
}
};
}])
Update: based on comments/discussion below:
For a service to be instantiated, it has to be injected somewhere – not just anywhere – somewhere where Angular accepts injectables. Just inject it into your getAnswers controller – .controller('getAnswers', function ($scope, $element, Romanize) – then pass it to your "controller" function: readingController($scope, Romanize).
Since I don't think readingController is a real Angular controller, you can name the arguments whatever you want, so scope should be fine.
Original attempt at an answer:
Inject $scope not scope into your controller:
var readingController = function ($scope, Romanize){
Then I don't get any errors: Plunker.