Define and Watch a variable according to windowidth - angularjs

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/

Related

Nested list is cut off by parent list element after slide animation

I'm building an Ionic app with nested lists of comments. I need to animate replies as child elements and retain state. Currently I'm using a simple directive with jQuery slideToggle, but this doesn't retain state (and isn't "the Angular way").
This example of slide animations by Shlomi Assaf is a great start to what I need, but it doesn't handle nested elements. I've created a nested version of his CodePen project to demonstrate the problem.
I'm not sure whether the animation function should be modified to handle nested elements, or whether my controller should call the animation on ancestor elements when the child element is animated (or after it has completed).
Assistance is appreciated. Here's the basis of the HTML using native AngularJS directives:
<button ng-click="slideToggle1=!slideToggle1">Click Me</button>
<div class="slide-toggle" ng-show="slideToggle1"> ... </div>
Here's the original animation function:
app.animation('.slide-toggle', ['$animateCss', function($animateCss) {
var lastId = 0;
var _cache = {};
function getId(el) {
var id = el[0].getAttribute("data-slide-toggle");
if (!id) {
id = ++lastId;
el[0].setAttribute("data-slide-toggle", id);
}
return id;
}
function getState(id) {
var state = _cache[id];
if (!state) {
state = {};
_cache[id] = state;
}
return state;
}
function generateRunner(closing, state, animator, element, doneFn) {
return function() {
state.animating = true;
state.animator = animator;
state.doneFn = doneFn;
animator.start().finally(function() {
if (closing && state.doneFn === doneFn) {
element[0].style.height = '';
}
state.animating = false;
state.animator = undefined;
state.doneFn();
});
}
}
return {
addClass: function(element, className, doneFn) {
if (className == 'ng-hide') {
var state = getState(getId(element));
var height = (state.animating && state.height) ?
state.height : element[0].offsetHeight;
var animator = $animateCss(element, {
from: {
height: height + 'px',
opacity: 1
},
to: {
height: '0px',
opacity: 0
}
});
if (animator) {
if (state.animating) {
state.doneFn =
generateRunner(true,
state,
animator,
element,
doneFn);
return state.animator.end();
} else {
state.height = height;
return generateRunner(true,
state,
animator,
element,
doneFn)();
}
}
}
doneFn();
},
removeClass: function(element, className, doneFn) {
if (className == 'ng-hide') {
var state = getState(getId(element));
var height = (state.animating && state.height) ?
state.height : element[0].offsetHeight;
var animator = $animateCss(element, {
from: {
height: '0px',
opacity: 0
},
to: {
height: height + 'px',
opacity: 1
}
});
if (animator) {
if (state.animating) {
state.doneFn = generateRunner(false,
state,
animator,
element,
doneFn);
return state.animator.end();
} else {
state.height = height;
return generateRunner(false,
state,
animator,
element,
doneFn)();
}
}
}
doneFn();
}
};
}]);
Here's what I'm using for now. It's a directive to set the parent element's height to auto so it expands or contracts with the child list height as it's toggled. If the parent list is toggled the height gets recalculated as normal for its animation.
app.directive('toggleParent', function () {
return {
restrict: 'C',
compile: function (element, attr) {
return function (scope, element) {
element.on('click', function (event) {
$(this).closest('.slide-toggle').css('height', 'auto');
});
}
}
}
});
CodePen demo
I'm sure that this same functionality can be implemented with the animation instead. That's what I'd really like help with.
if you want to do more than one level deep, you can use this:
jQuery
app.directive('toggleParents', function () {
return {
compile: function (element, attr) {
return function (scope, element) {
element.on('click', function (event) {
$(this).parents('.slide-toggle').css('height', 'auto');
});
}
}
}
});
Native JS
app.directive('toggleParents', function() {
return {
compile: function(element, attr) {
var parents = function(el, cls) {
var els = [];
while (el = el.parentElement) {
var hasClass = el.classList.contains(cls);
if (hasClass) {
els.push(el);
}
}
return els;
};
return function(scope, element) {
element.on('click', function(event) {
angular.element(parents(element[0], 'slide-toggle')).css('height', 'auto');
});
};
}
};
});
You only need to add else statement here:
if (closing && state.doneFn === doneFn) {
element[0].style.height = '';
} else {
element[0].style.height = 'auto';
}
This way after the animation is done, height is set to auto and that solves everything on as many levels as you want.

Angular upload file directive

I need a directive for upload file (brwose to choose file, and get it as binary data ) , in my angular application.
I use it a lot of times, so it has to be generic.
What is the best way to do it?
https://github.com/flowjs/ng-flow
This works fine.Of course some wrapping directive can be made at your side.
directive code
.directive("fileReaderGallery", ['background', function (background) {
return {
restrict: "E",
scope: true,
templateUrl: "templates/directive.html",
replace: true,
link: function ($scope, el, attrs) {
var input = null,
drag_image_gallery = el.find('.drag_image_gallery');
$scope.dragging = false;
$scope.fileselected = false;
$scope.uploaded = false;
$scope.uploading = false;
$scope.image = null;
$scope.clearFileReader = function () {
if (!attrs.styling || !input) return false;
$scope.formTitan.elementStyles[attrs.styling][$scope.styledItem.pt]['background-image'] = '';
$scope.formSelected[attrs.styling].imageFile = null;
$scope.formSelected[attrs.styling].isImage = false;
input.value = '';
$scope.fileselected = false;
$scope.imageName = '';
};
var readfiles = function (files) {
var reader, bg;
if (files && files[0]) {
if (files.length > 1) {
return console.log("Select single file only");
}
reader = new FileReader;
reader.onload = function (e) {
if (files[0].type.indexOf('image/') !== -1) {
if (e.target.result) {
bg = {
'background-image': e.target.result,
'background-repeat': 'repeat',
'background-position': 'top',
'background-size': ''
};
$scope.uploading = true;
$scope.$apply(function () {
background.add(angular.copy(bg));
$scope.current.dcBGImage = angular.copy(bg);
$scope.imageName = files[0].name;
$scope.image = e.target.result;
$scope.fileselected = true;
console.log(files[0])
});
}
} else {
return console.log('Please select an Image');
}
};
return reader.readAsDataURL(files[0]);
}
};
$scope.clickUpload = function () {
el.find('.bg-file-reader').click();
};
drag_image_gallery[0].ondragover = function () {
$scope.dragging = true;
//drag_image_gallery.addClass('ondragover');
$scope.$digest();
return false;
};
drag_image_gallery[0].ondragleave = function () {
$scope.dragging = false;
$scope.$digest();
//drag_image_gallery.removeClass('ondragover');
};
drag_image_gallery[0].ondrop = function (e) {
$scope.dragging = false;
$scope.$digest();
//drag_image_gallery.removeClass('ondragover');
e.preventDefault();
readfiles(e.dataTransfer.files);
};
el.find('.bg-file-reader').on('change', function () {
readfiles(this.files);
});
}
};
}]);
html template code
<div class="row upload_image text-center">
<div class="drag_image drag_image_gallery row text-center" ng-class=" {ondragover:dragging}">
Drag an Image here
</div>
OR
<div class="row text-center choose_computer">
<button ng-click="clickUpload()" class="btn btn-default">Choose from your computer</button>
<input type="file" class="bg-file-reader upload" name="gallery"/>
</div>
</div>
directive with drag and drop and chose file both functionality

AngularJS detect Bootstrap Environment

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

AngularJS $watch newValue is undefined

I am trying to make a alert service with a directive. It is in the directive part I have some trouble. My have a directive that looks like this:
angular.module('alertModule').directive('ffAlert', function() {
return {
templateUrl: 'components/alert/ff-alert-directive.html',
controller: ['$scope','alertService',function($scope,alertService) {
$scope.alerts = alertService;
}],
link: function (scope, elem, attrs, ctrl) {
scope.$watch(scope.alerts, function (newValue, oldValue) {
console.log("alerts is now:",scope.alerts,oldValue, newValue);
for(var i = oldValue.list.length; i < newValue.list.length; i++) {
scope.alerts.list[i].isVisible = true;
if (scope.alerts.list[i].timeout > 0) {
$timeout(function (){
scope.alerts.list[i].isVisible = false;
}, scope.alerts.list[i].timeout);
}
}
}, true);
}
}
});
The reason for the for-loop is to attach a timeout for the alerts that has this specified.
I will also include the directive-template:
<div class="row">
<div class="col-sm-1"></div>
<div class="col-sm-10">
<div alert ng-repeat="alert in alerts.list" type="{{alert.type}}" ng-show="alert.isVisible" close="alerts.close(alert.id)">{{alert.msg}}</div>
</div>
<div class="col-sm-1"></div>
</div>
When I run this, I get this error in the console:
TypeError: Cannot read property 'list' of undefined
at Object.fn (http://localhost:9000/components/alert/ff-alert-directive.js:10:29)
10:29 is the dot in "oldValue.list" in the for-loop. Any idea what I am doing wrong?
Edit: I am adding the alertService-code (it is a service I use to keep track of all the alerts in my app):
angular.module('alertModule').factory('alertService', function() {
var alerts = {};
var id = 1;
alerts.list = [];
alerts.add = function(alert) {
alert.id = id;
alerts.list.push(alert);
alert.id += 1;
console.log("alertService.add: ",alert);
return alert.id;
};
alerts.add({type: "info", msg:"Dette er til info...", timeout: 1000});
alerts.addServerError = function(error) {
var id = alerts.add({type: "warning", msg: "Errormessage from server: " + error.description});
// console.log("alertService: Server Error: ", error);
return id;
};
alerts.close = function(id) {
for(var index = 0; index<alerts.list.length; index += 1) {
console.log("alert:",index,alerts.list[index].id);
if (alerts.list[index].id == id) {
console.log("Heey");
alerts.list.splice(index, 1);
}
}
};
alerts.closeAll = function() {
alerts.list = [];
};
return alerts;
});
try like this , angular fires your watcher at the first time when your directive initialized
angular.module('alertModule').directive('ffAlert', function() {
return {
templateUrl: 'components/alert/ff-alert-directive.html',
controller: ['$scope','alertService',function($scope,alertService) {
$scope.alerts = alertService;
}],
link: function (scope, elem, attrs, ctrl) {
scope.$watch(scope.alerts, function (newValue, oldValue) {
if(newValue === oldValue) return;
console.log("alerts is now:",scope.alerts,oldValue, newValue);
for(var i = oldValue.list.length; i < newValue.list.length; i++) {
scope.alerts.list[i].isVisible = true;
if (scope.alerts.list[i].timeout > 0) {
$timeout(function (){
scope.alerts.list[i].isVisible = false;
}, scope.alerts.list[i].timeout);
}
}
}, true);
}
}
});
if you have jQuery available, try this
if(jQuery.isEmptyObject(newValue)){
return;
}
inside scope.$watch

angularjs autosave form is it the right way?

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

Resources