In Angularjs, how do I review the DOM after a route change? - javascript

In my app I use ng-view to switch out my views. I need to manipulate elements such as change width and position. When I try to do it with on my controller I'm getting, as expected, Unable to get property 'setAttribute' of undefined or null reference.
I know this is happening since JS doesn't know the DOM has changed.
Since I'm running some heavy plugins to work with SQLite in Windows 8 I can't post too much code.
This is my template being loaded into the ng-view
<div id="productslist" class="container">
<div class="product" ng-repeat="product in products">
<div class="details">
<img src="img/detail-list-back.png" class="texture" />
<div class="heading">
<p class="title">{{product.name}}</p>
<img src="img/products/title-shadow.png" class="title-shadow"/>
</div>
<div class="text">
<div class="text-container">
<p class="intro" ng-hide="product.intro == null">{{product.intro}}</p>
<p class="title">Curves</p>
{{product.prodText}}
</div>
</div>
</div>
<div class="image" style="background-image:url('img/products/nanoslim-bkg.jpg')"></div>
</div>
</div>
some of my angular. the for loop is breaking since it doesn't know the sections exist:
var totalProd = res.rows.length;
var windowW = window.innerWidth;
var sections = document.querySelectorAll('#productslist .product');
var textArea = document.querySelectorAll('#productslist .text');
document.getElementById('productslist').setAttribute('style', 'width:' + (windowW * totalProd) + 'px');
for (var i = 0; i < sections.length; i++) {
sections[i].setAttribute('style', 'width:' + windowW + 'px');
}
Everything else works fine. I'm trying to bypass this by using ng-style but having issues there.

In angular, DOM manipulations should be done in directives where possible. Here's an example of setting the width to the window width:
.directive('fullWidth', function() {
function link(scope, element, attrs) {
var windowW = window.innerWidth;
element.css({width: windowW + 'px'});
}
return {
link: link
};
});
Now in your view
<div class="product" full-width ng-repeat="product in products">

Assuming that you have a different controller for each distinct view, there are possibilities that your DOM takes more time to load then your controller, leading to the issue Unable to get property 'setAttribute' of undefined or null reference.
You can instead put your DOM selector statement in a $timeout statement, with a delay of 1 or 2 seconds, that will give DOM enough time to load and prevent any such null reference issue. Something like this :
myapp.controller("mycontroller"), function($scope, $timeout) {
$timeout(function() {
$("your-dom-selector").setAttribute("height", 500px);
}, 2000);
});
Alternatively a better approach would be to use a broadcast model, in which you can track the route change event, and as soon as the route change event succeeds, you can broadcast an event, with necessary details, that can be captured by the respective controller.
Former approach is easy to use, latter one is standard and guaranteed to be error-free.
More details upon $timeout here

Related

Adding css after creating elements

I want to add some icons to elements I created with angularJS directly after creating them.
So I am calling the function to set the icons at the same time the elements were created.
data-ng-click="opeTab($event); getObjects($event); loadObj($event); setIcons();"
The problem is, I can get the elements with:
$scope.setIcons = function(){
var tbs = document.getElementsByClassName("tabTr");
for(let i = 0; i < tbs.length; i++){
console.log(i);
tbs[i].style.backgroundImage = "url('../ICONS\Icons_24\'" + tbs[i].id + "')";
}
}
And the list in the console is filled, but the length of the array is 0.
So what possibility do I have to "wait" for the creation except setting a timeout?
You should try to avoid creating elements yourself from your controllers. Maybe you have a good reason for doing this, but I can't see that from the example you have given.
Somewhere in your template you should have an ng-repeat which renders your tabs. Each tab should have an ng-style. Lets say:
// template.html
<div class="tabs" ng-repeat="tab in tabs">
<div
class="tab"
ng-style="getBackgroundImageStyle(tab.id)">
tab {{ tab.id }}
</div>
</div>
// controller.js
$scope.tabs = [];
$scope.getBackgroundImageStyle = tabId => `{
'background-image': 'url('../ICONS/Icons_24/${tabId}')'
}`
$scope.openTab = () => {
$scope.tabs.push(new Tab(nextTabId)); // or however you create your tabs
}
If you have a good reason for accessing the dom directly like this, then there is no problem using $timeout with a delay of 0 and wrapping your dom modification inside this. Everything should be rendered before the code inside your $timeout runs.

pushing dynamically created dropped elements at the particular position in the dom tree

Hello people I have a question regarding drag and drop . I am creating an application for creating websites only by using drag and drop . I am kinda stuck implementing a feature in which the user can drop elements at a particular positions and the elements below that dropped element get shifted below . Now the issue is that element is generated dynamically
for eg . In the right panel there are different components in the form of images and when i drag those elements I pass some meta-data and when the element gets dropped , based on the meta-data an html element is created .
<div class="drop-zone">
<div>
<!--container 1-->
</div>
<!-- now i want to drop that element in between which is generated dynamically -->
<div>
<!--container 2-->
</div>
</div>
*****************EDIT*****************************
In the below code header drop is the directive which will be dragged , JSONfn is same as JSON object used to stringify the functions as well
(function(){
define(['../../../../app','./../service/header.factory.js'],function(app){
app.directive('headerDrop',['HeaderFactory',function(HeaderFactory){
return {
restrict: "E",
replace: false,
scope: {},
link: function(scope,element,attrs) {
element.on('dragstart',function(e){
e.originalEvent.dataTransfer.setData("data",JSONfn.stringify(HeaderFactory));
});
},
template: "<img id='header' draggable='true' src='/src/create_template/images/noimage.jpg' width='100' height='100'> </img>"
}
}]);
app.directive('dragContainer',function(){
return {
restrict: "E",
replace: false,
scope: {},
template: "<div id='elements-container'> <h1 style='text-align:center;'> All </h1><header-drop> </header-drop> </div>"
}
});
});
})()
Inside the controller
element.on('drop',function(event){
console.log(event);
if(event.target.className !== "drop-zone"){
event.preventDefault();
return false;
}
var data = JSONfn.parse(event.originalEvent.dataTransfer.getData("data"));
if(data.type=="header"){
var heading = document.createElement("h1");
console.log("client height" , heading.clientWidth);
heading.innerHTML = data.textValue;
//console.log("pageY",event.pageY);
//console.log("pageX",event.pageX);
heading.style.position = "relative";
heading.style.top = ((event.pageY/window.innerHeight)*100)+ "%";
heading.className = "editable";
event.target.appendChild(heading);
heading.style.top = event.clientY;
addingEvents();
}
});
Well i actually tackled that problem with a naive approach though i found the answer later .
I was making a template generator so this approach worked for me
Naive Approach
What i did was , whenever i was dropping the element , i dropped one more block inside the designer of height 5px just above the element kinda like
<div class="adjacent-block"> </div> inside css .adjacent-block {min-height: 5px; }
<div class="dropped-block"> </div>
Then drop that element inside the adjacent block .
Disadvantage : You have also keep track of the adjacent block
THE ABOVE METHOD WAS PRETTY PATHETIC AND I WOULDN'T RECOMMEND ANYONE TO DO THAT
EASIER AND BETTER APPROACH:
Inside the dragover event just check if the element is above another element and drop the block before or after the element according to the position of the mouse from the mid of that element .
I haven't implemented this approach but I am pretty sure this would work

Select specific div and get outerHeight with Angular's jqLite

I am trying to rid my Webapp of jQuery and only use Angular's jqLite, since there isn't that much I need to do with jQuery anyway and I've read several times that using jQuery with AngularJS is not pretty.
My problem is that I need to select divs with specific id's, but jqLite only supports search by tag names as far as I can see. Furthermore, I need .outerHeight(), which also seems to be absent. So naturally, my current code doesn't work:
app.directive('contentMargin', function($document, $window) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
$document.ready(function() {
var topMargin = elem.find('#header').outerHeight(true);
var bottomMargin = elem.find('#nav').outerHeight(true);
elem.find('#content').css({'margin-top':topMargin, 'margin-bottom':bottomMargin});
});
}
};
});
Here is where I use the directive:
<div id="wrapper" content-margin>
<!---Header--->
<div id="header">
<div id="logo" class="fullwidth">
<img src="images/logo.jpg"/>
</div>
</div>
<!---Content--->
<div id="content" class="container-fluid">
<div ng-view></div>
</div>
<!---Footer Nav--->
<div id="nav" class="fullwidth" ng-controller="MainController">
<ul class="list-unstyled">
[...]
</ul>
</div>
</div>
Is there any way to achieve this without full jQuery?
mate, I suggest you use Vanilla JS, it's powerful than you thought...
the equivlent code in Vanilla Js is below:
function Dimension(elmID) {
var elmHeight, elmMargin, elm = document.getElementById(elmID);
if(document.all) {// IE
elmHeight = elm.currentStyle.height;
elmMargin = parseInt(elm.currentStyle.marginTop, 10) + parseInt(elm.currentStyle.marginBottom, 10) + "px";
} else {// Mozilla
elmHeight = document.defaultView.getComputedStyle(elm, '').getPropertyValue('height');
elmMargin = parseInt(document.defaultView.getComputedStyle(elm, '').getPropertyValue('margin-top')) + parseInt(document.defaultView.getComputedStyle(elm, '').getPropertyValue('margin-bottom')) + "px";
}
return (elmHeight+elmMargin);
}
In your case, your code looks like:
var topMargin = Dimension('header');
var bottomMargin = Dimension('nav');
I've read most of Jquery/Jquery UI source code, actually, they are just plain code, and sometimes you can do better than them. lol, happy coding bro!

How to change attribute of html in angular js?

I want to change attribute of a div in angularjs. I know how to do in jquery but not in angular.
html :
<div ng-app="test">
<div ng-controller="cont">
<button ng-click="updateStatus()">TOGGLE ATTRIBUTE </button>
<div id="test" {{status}}>TEXT </div>
</div>
</div>
js :
var angApp = angular.module('test',[]);
angApp.controller('cont', ['$scope', function($scope){
$scope.status = 'someattr';
$scope.updateStatus = function() {
if( $scope.status == 'someattr'){
$scope.status = '';
}else{
$scope.status = 'someattr';
}
};
}])
Here is jsfiddle to work with.
In jquery :
var div = $('#test');
$('button').on('click',function(){
if( div.attr('someattr'){
div.removeAttr('someattr');
}else{
div.attr('someattr',true);
}
})
I want to achive same in angularjs.
NOTE : I AM NOT TRYING TO ADD DISABLED STATE TO DIV. I JUST WANT TO TOGGLE AN ATTRIBUTE.
In your specific case (add disabled attribute), you have to use ng-disabled in order to bind its value to a $scope variable.
It makes no sense to use it on a div, I'll use a button instead to give you an example:
<button ng-click="updateStatus()">TOGGLE ATTRIBUTE </button>
<button id="test" ng-disabled='status'>TEXT</button>
see a working example HERE
UPDATE
To toggle an attribute, yo can use attr() and removeAttr():
el.attr("disabled", "true");
el.removeAttr("disabled");
See a complete example HERE
NOTE (thanks to jme11): as reported on Angular Dev Guide
Do not use controllers to:
Manipulate DOM — Controllers should contain only business logic. Putting any presentation logic into Controllers significantly affects its testability. Angular has databinding for most cases and directives to encapsulate manual DOM manipulation.
you should avoid to manipulate the DOM inside the controller.
Make a directive which uses .attr and .removeAttr in a $watch handler. Here's a modified version of your fiddle: http://jsfiddle.net/0eqz1qo1/1/
The directive:
.directive('addAttr', function() {
return function(scope, elem, attr) {
scope.$watch(attr.addAttr, function(val, prev) {
if(val)
elem.attr(val, "");
if(prev && prev !== val)
elem.removeAttr(prev);
});
}
})
Usage:
$scope.myVar = 'hello';
...
<div add-attr="myVar"></div>
becomes:
<div add-attr="myVar" hello></div>
You can not implement disable property for any div.But you can hide or show the div using Angular.js.Check the code below.
<div ng-app="test">
<div ng-controller="cont">
<button ng-click="updateStatus()">TOGGLE ATTRIBUTE </button>
<div id="test" ng-hide="hide-div" >TEXT </div>
</div>
</div>
JS:
var angApp = angular.module('test',[]);
angApp.controller('cont', ['$scope', function($scope){
$scope.hide-div = true;
$scope.updateStatus = function() {
if( $scope.hide-div == true){
//do something here
}else{
$scope.hide-div = true;
}
};
}])
Other option is you can also use ng-class in div and inside those class you can declare display:none,display:block
You can't add an attribute by this way with angularJS. If you inspect your code, you can see that the attribute that you're trying to pass in div is {{status}}(your expression), use existing attributes rather than create your own! For example: ng-disabled, ng-show, ng-hide.
It's not really right thing to do. I guess, cannot inject attribute with angularJS. {{status}} is an expression, it's like expression and will evaluate by angularjs while rendering to html. about expression: https://docs.angularjs.org/guide/expression
Replace your line :
<div id="test" {{status}}>TEXT </div>
with these :
<div id="test" someattr="" ng-if="status=='someattr'" >TEXT</div>
<div id="test" ng-if="status==''" >TEXT</div>

Structure issues using ng-repeat with Bootstrap and AngularJS

I'm building an app with Bootstrap and AngularJS. At some point I have an ng-repeat on a col-md-3, listing products. My problem is that I want to be able to insert a collapse into the grid, but as the columns are automatically generated, I don't really know how to do it.
Here's a diagram to understand it better:
First, the grid of .col-md-3 is populated from the ng-repeat.
And what I'm trying to achieve, is to add a .col-md-12 that appears right under the row of the .col-md-3 that gets clicked on.
My initial thought was to add an empty .col-md-12 dynamically after each group of 4 .col-md-3, but I wouldn't know how to do so, and it kinda seems to be that it would be a rather dull approach. Any ideas?
Here's the relevant html:
<div class="infinite" infinite-scroll="loadDetails()">
<div class="col-xs-3 col-md-3" ng-repeat="release in main.releases | filter:main.album">
<release release="release" artist="main.artist" class="dropdown"></release>
</div>
</div>
EDIT: Here's a working Plunker including tasseKATTs solution.
Place a custom directive on your inner element together with a position counter that starts with 1 and a marker describing if it's the last element:
<div ng-repeat="item in items" class="col-xs-3">
<div class="item" the-directive position="{{ $index + 1 }}" last="{{ $last }}">
</div>
</div>
Create the directive with an isolated scope, bind scope properties to the values of the position and last attributes and attach a click event handler to the element:
app.directive('theDirective', function() {
return {
restrict: 'A',
scope: { position: '#', last: '#' },
link: function(scope, element, attrs) {
element.bind('click', function() {
...
});
}
};
});
In the click handler first create the collapse element or select it if it already exists:
var collapseQuery = document.querySelector('#collapse');
var collapse = collapseQuery === null ?
angular.element('<div id="collapse" class="col-xs-12"><div class="twelve"></div></div>') :
angular.element(collapseQuery);
Based on the position of the clicked element calculate the rounded number up to the nearest multiple of four:
var calculatedPosition = Math.ceil(scope.position / 4) * 4;
Get the element at the calculated position or the last one if the position is out of range:
var calculatedQuery = document.querySelector('[position="' + calculatedPosition + '"]');
if (calculatedQuery === null) calculatedQuery = document.querySelector('[last="true"]');;
var calculatedElement = angular.element(calculatedQuery);
Insert the collapse element after the element at the calculated position:
calculatedElement.parent().after(collapse);
Could use some optimizations, but hopefully puts you on the right track.
Demo with some extra visuals: http://plnkr.co/edit/fsC51vS7Ily3X3CVmxSZ?p=preview
This question is easier to answer in an angular way if you follow the bootstrap convention using 12 columns per a row:
Grid columns are created by specifying the number of twelve available columns you wish to span. For example, three equal columns would use three .col-xs-4.
In your case, this means each row can have up to 4 .col-xs-3 columns, or just 1 .col-xs-12. You can prep your data to be displayed this way by splitting it into an array of smaller arrays.
$scope.getRows = function(array) {
var rows = [];
//https://stackoverflow.com/questions/8495687/split-array-into-chunks
var i,j,temparray,chunk = 4;
for (i=0,j=array.length; i<j; i+=chunk) {
temparray = array.slice(i,i+chunk);
rows.push(temparray);
}
return rows;
};
$scope.rows = $scope.getRows($scope.main.releases);
Then you can nest ngRepeat to achieve the desired layout, using ng-if to only create a col-xs-12 when a corresponding .col-xs-3 is clicked.
<div ng-repeat="row in rows">
<div class="row">
<div class="col-xs-3" ng-repeat="release in row" ng-click="main.releaseClicked=release">
<div class="release">{{release}}</div>
</div>
</div>
<div class="row" ng-repeat="release in row" ng-if="main.releaseClicked==release">
<div class="col-xs-12">
<div class="detail">Release detail: {{release}}</div>
</div>
</div>
</div>
This leaves you with a more declarative view that describes how the app works, and doesn't require jQuery to do DOM manipulation.
Here is a working demo: http://plnkr.co/ujlpq5iaX413fThbocSj

Categories