AngularJS C directive using ng-class - css

The title might not explain what I am trying to achieve, so I will elaborate here.
I have a directive that is restricted to a CSS class name (in this example flex-wrap).
But this class is not applied to the element until we actually have some data.
The HTML for that looks like this:
<div class="row" ng-class="{ 'loading': !controller.loadingRecent, 'flex flex-vertical flex-wrap': controller.recent.length }">
<div class="col-md-12 row-title">
<h1>Recent orders</h1>
</div>
<div class="col-xs-12" ng-if="!controller.recent.length">
<div alert type="danger">
No records have been found that match your search.
</div>
</div>
<div class="col-md-4 tile-lg" ng-repeat="order in controller.recent" tile>
<a class="box-shadow" id="{{ order.orderNumber }}" ui-sref="viewOrder({ orderNumber: order.orderNumber })" coloured-tile>
<div class="text">
<p>
<strong>{{ order.account.accountNumber }}</strong><br />
{{ order.account.name }}<br />
{{ order.raisedBy }}<br />
{{ order.orderNumber }}<br />
{{ controller.getDescription(order) }}<br />
</p>
</div>
</a>
</div>
As you can see, the flex classes are not applied until our recent.length is greater than 0. What I would like to happen is that when we have records, the CSS class is applied and so the angular directive that is associated with that class fires.
Instead, it doesn't do anything at the moment.
Does anyone know how I can get my directive to fire?
Here is my directive, just so you can see it.
.directive('flexWrap', ['$window', '$timeout', function ($window, $timeout) {
// Sets the height of the element
var setHeight = function (element) {
// Declare our variables
var row = element.parent().parent(),
height = 630;
// If our row is a row
if (row.hasClass('row')) {
// Get the height of the rest of the items
height = height - getHeight(row);
}
console.log('height = ' + height);
// Set our elements height
element.css('height', height + 'px');
console.log('we are about to set the width');
// After we set the height, set the width
setWidth(element);
}
// Gets the height to minus off the total
var getHeight = function (element) {
// Declare our variables
var height = 0,
children = element.children(),
loopChildren = element.hasClass('row');
// Loop through the element children
for (var i = 0; i < children.length; i++) {
// Get the child
var child = angular.element(children[i]);
// If the child is not a column
if (!child.hasClass('columns')) {
// If we need to loop the children
if (loopChildren) {
// Get the height of the children
height += getHeight(child);
// Otherwise
} else {
// Add the height of the child to
height += child[0].offsetHeight;
}
}
}
// Return our height
return height;
};
// Sets the width of the element
var setWidth = function (element) {
// After a short period
$timeout(function () {
// Get our last child
var children = element.children(),
length = children.length,
lastChild = children[length - 1];
// Work out the width of the container
var position = element[0].getBoundingClientRect(),
childPosition = lastChild.getBoundingClientRect(),
width = childPosition.left - position.left + childPosition.width;
var style = $window.getComputedStyle(lastChild, null);
console.log(style.getPropertyValue('width'));
console.log('--------------------------------');
console.log(lastChild);
console.log(position);
console.log(childPosition);
console.log(width);
console.log('--------------------------------');
console.log('width = ' + width);
// Apply the width to the element
element.css('width', width + 'px');
}, 500);
};
// Resize the container
var resize = function (element, width) {
// If our width > 992
if (width > 992) {
// Resize our element
setHeight(element);
// Otherwise
} else {
// Set our element width and height to auto
element.css('height', 'auto');
element.css('width', 'auto');
}
};
return {
restrict: 'C',
link: function (scope, element) {
// Get our window
var window = angular.element($window),
width = $window.innerWidth;
// Bind to the resize function
window.bind('resize', function () {
// After half a second
$timeout(function () {
// Get the window width
width = $window.innerWidth;
// Resize our element
resize(element, width);
}, 500);
});
// Initial resize
resize(element, width);
}
};
}]);

Directive declaration style (e.g. restrict: "C") and an ng-class directive are not related to each other at all.
ng-class just adds/removes CSS class - it does not trigger a compilation/link of a directive that might be associated with these classes. In other words, it does not provide a way to dynamically instantiate a directive.
Your directive should handle the situation where data is not yet available. There are a number of ways to achieve that, via $scope.$broadcast/$scope.$on or via a service, or even via $watch - depending on any particular situation.

Related

Making a button that shows or hides multiple images in a random location

I have a problem when I am making the website for one gallery.
I made the code for the button that can show and hide multiple images.
I intend to make the button can place several images in randomly.
I write the code that can function for only one image.
Please tell me the code that functions as a button to place multiple images in a random location.
Users can hide images by pressing the button.
And when users press the button again, it places the images in another random location.
const btn = document.querySelector("button");
const height = document.documentElement.clientHeight;
const width = document.documentElement.clientWidth;
const box = document.getElementById("color");
btn.addEventListener("click", () => {
let randY = Math.floor((Math.random() * height) + 1);
let randX = Math.floor((Math.random() * width) + 1);
box.style.top = randY + "px";
box.style.right = randX + "px";
});
function showhide() {
var x = document.querySelectorAll("#color");
var i;
for (i = 0; i < x.length; i++) {
if (x[i].style.display === "block") {
x[i].style.display = "none";
} else {
x[i].style.display =
"block";
}
}
}
body {
height: 500px;
}
.random {
position: absolute;
}
<button onclick="showhide()" value="Zeige Features" id="button">click me</button>
<img id="color" style="display: none;" class="random" src="http://lorempixel.com/200/200/">
<img id="color" style="display: none;" class="random" src="http://lorempixel.com/200/200/">
You're doing the correct thing in showHide() when using querySelectorAll. You are then able to get all images.
You should never have elements with the same ids. They should be unique. So querySelectorAll("#color") works, but it's now how you should do. Do a querySelector on "img.random" instead.
getElementById only returns a single element, not like querySelectorAll. So you need to use querySelectorAll('img.random').
This might be beyond your knowledge, I don't think you should add the images in HTML, but in javascript code.
a) Add all image paths in an array: ['https://image.com/image.png', ...]
b) Add a single img element. <img id="template" class="random">
c) In javascript code, clone that element for each image path in the array. You can use cloneNode for this.
d) Randomize each position for each element, just like you have done now.
e) Add each element to the DOM through appendChild. Have a unique div that you append to. Be sure to clear it every time second time you hit the button.
f) Solve all bugs along the way. :P
The problem
The main issue here is that you're using getElementById to query #color
const box = document.getElementById("color");
Since getElementById only returns one element (but you have two in your DOM) and the style only applies to one element. That's why you're seeing only one element is randomly moving and the other just stay in the same place.
A side note here, id should be unique in a DOM.
You're in fact using the correct API for the job in the showhide function
var x = document.querySelectorAll("#color");
The fix:
To fix this, you need to query all images by their classname (as suggested in the side note, don't use id for the job)
const boxes = document.querySelectorAll(".random");
Now we have a node list, as you do in the showhide function, we need to loop thru it, I'm not using a for loop here, instead, a forEach loop, it's just more terser and a modern addition to the JS
// Since boxes are not array, we need to covert it to array so we can use that handy `.forEach` here:
Array.from(boxes).forEach(box => {
box.style.top = Math.floor((Math.random() * height) + 1) + "px";
box.style.right = Math.floor((Math.random() * width) + 1) + "px";
})
Now, this should fix your issue. See the complete code below.
const btn = document.querySelector("button");
const height = document.documentElement.clientHeight;
const width = document.documentElement.clientWidth;
const boxes = document.querySelectorAll(".random");
btn.addEventListener("click", () => {
Array.from(boxes).forEach(box => {
box.style.top = Math.floor((Math.random() * height) + 1) + "px";
box.style.right = Math.floor((Math.random() * width) + 1) + "px";
})
});
function showhide() {
var x = document.querySelectorAll(".random");
var i;
for (i = 0; i < x.length; i++) {
if (x[i].style.display === "block") {
x[i].style.display = "none";
} else {
x[i].style.display =
"block";
}
}
}
body {
height: 500px;
}
.random {
position: absolute;
}
<button onclick="showhide()" value="Zeige Features" id="button">click me</button>
<img id="color" style="display: none;" class="random" src="http://lorempixel.com/200/200/">
<img id="color" style="display: none;" class="random" src="http://lorempixel.com/100/100/">

$timeout in a directive link function

So I'm using this directive to collapse a variable height card in Ionic. The directive grabs the auto height and changes it to a defined height so it can then be collapsed to 0 with a css animation. It was working fine for my needs, but now I need to use ng-src to dynamically load an image within the card. What's happening is the image is being loaded after the directive, so the image loads and overflows the card.
Directive:
.directive('collapse', ['$timeout', function ($timeout) {
return {
restrict: 'A',
link: function ($scope, ngElement, attributes) {
var element = ngElement[0];
$timeout(function(){
$scope.$watch(attributes.collapse, function (collapse) {
var newHeight = collapse ? 0 : getElementAutoHeight();
element.style.height = newHeight +"px";
ngElement.toggleClass('collapsed', collapse);
});
function getElementAutoHeight() {
var currentHeight = getElementCurrentHeight();
element.style.height = 'auto';
var autoHeight = getElementCurrentHeight();
element.style.height = currentHeight +"px";
getElementCurrentHeight(); // Force the browser to recalc height after moving it back to normal
return autoHeight;
}
function getElementCurrentHeight() {
return element.offsetHeight
}
});
}
};
}])
and HTML:
<div ng-repeat="item in items | orderBy : '-'" collapse="item.deleted">
<div class="list card">
<img class="full-image" ng-src="{{item.image}}"/>
</div>
</div>
As you can see I've injected $timeout and leaving the interval blank in hopes it will wait until the DOM is loaded, but it seems no matter how I use it, the directive still explicitly sets the height of the element in css before the image child element is rendered. How can I delay the setting of element height until after ng-src is loaded in each ng-repeat item?
First thing, angular $timeout without an $interval will not wait for the DOM tree to load, basically, what it does is waiting for the current digest cycle to finish before executing the function in the first parameter. By doing so, it will allow the your code to wait till the directive finish compile and render before calculating the height of the div.
However, there is no guarantee that the image will be loaded by that time. Images are loaded by the browser independently from DOM rendering, therefore, to calculate the height of the container having images precisely, you should make use of JS Image Object and the load event. Once the images are fully loaded, then you can update the height.
Also, for your directive, I don't think you need to calculate the height every times the collapse variable changed (inside the watch), you can simply wait till the image being loaded, calculate the height once, store it inside the scope object, and reuse it whenever the collapse variable change.
Okay, so thanks to Thai's input, I have a working solution:
.directive('collapse', [function () {
return {
restrict: 'A',
link: function ($scope, ngElement, attributes) {
var element = ngElement[0];
var img = element.querySelector('.full-image');
angular.element(img).bind('load', function() {
var autoHeight = getElementAutoHeight();
element.style.height = autoHeight + "px";
});
$scope.$watch(attributes.collapse, function (collapse) {
var newHeight = collapse ? 0 : getElementAutoHeight();
element.style.height = newHeight +"px";
ngElement.toggleClass('collapsed', collapse);
});
function getElementAutoHeight() {
var currentHeight = getElementCurrentHeight();
element.style.height = 'auto';
var autoHeight = getElementCurrentHeight();
element.style.height = currentHeight +"px";
getElementCurrentHeight(); // Force the browser to recalc height after moving it back to normal
return autoHeight;
}
function getElementCurrentHeight() {
return element.offsetHeight
}
}
};
}])

What element is jQuery UI draggable being dragged over in an iframe

Here is my code, where I'm trying to detect the element, which a jQuery UI draggable is hovering over. I need to get the element's object and attributes, such as class names (in this case .sortable-grid,.sortable-table,.sortable-row,.sortable-cell).
The answers found here only show how to get the draggable item itself (ui.helper or event.target), but not the element it is hovering above.
The best way to answer would be using the prepared JSFiddle, since my code uses an iframe, which would not work if the full code is posted here:
JSFiddle
HTML:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.0-beta.1/jquery-ui.js"></script>
<div style="background-color:grey;display:inline;cursor:move" id="draggable">DRAG ME</div>
<iframe src="https://fiddle.jshell.net/piglin/UAcC7/1869/show/" id="frame" style="width:100%;overflow:visible" seamless="seamless" scrolling="no"></iframe>
JS:
$("#draggable").draggable({
drag: function(event, ui) {
//Some code here
}
}
It was possible by modifying the function from another answer to fit this purpose. After adapting it to use the contentWindow of the iframe and adding offset calculation it works now.
Solution
function allElementsFromPointIframe(x, y, offsetX, offsetY) {
var element, elements = [];
var old_visibility = [];
while (true) {
element = document.getElementById('frame').contentWindow.document.elementFromPoint(x - offsetX, y - offsetY);
if (!element || element === document.getElementById('frame').contentWindow.document.documentElement) {
break;
}
elements.push(element);
old_visibility.push(element.style.visibility);
element.style.visibility = 'hidden'; // Temporarily hide the element (without changing the layout)
}
for (var k = 0; k < elements.length; k++) {
elements[k].style.visibility = old_visibility[k];
}
elements.reverse();
return elements;
}
var selected = $('');
var tmpColor = 'transparent';
$("#draggable").draggable({
drag: function(event, ui) {
var el = $(allElementsFromPointIframe(event.pageX, event.pageY, $(frame).offset().left, $(frame).offset().top));
var div = $(el).filter('ul, li').not($(this));
selected.css({'backgroundColor': tmpColor});
selected = div.last()
tmpColor = selected.css('backgroundColor');
selected.css({'backgroundColor': 'red'});
console.dir(div);
},
iframeFix: true,
iframeOffset: $('#iframe').offset()
});

How to position an element absolute to the window regardless of its DOM position?

I am creating a pop-up overlay modal and am having problems getting the positioning/scrolling working correctly.
I can set my modal to be position:fixed but then if the modal's height is too much, then the modal overflows off of the window and you cannot see the bottom of it.
If I set the modal to be position:absolute then the element becomes positioned relative to the closest ancestor with position:relative, correct? (or at least thats what it appears to do) Instead I want the modal to ALWAYS be relative to the window so that I can center it easily.
Is there a way to make the below .modal positioned relative to the window ( or element) even if the element is nested deep inside the DOM like this:
<body ng-app="myapp" ng-controller="mycontroller">
<div>
<div>
<div ui-view>
<div class=".modal"></div>
</div>
</div>
</div>
</body>
If you insist on having it in that same markup and nested in the same manner, your best bet is in JavaScript.
Here's some JS code that gives a good method of accomplishing what you asked for:
function ShowDivInCenter()
{
try
{
divWidth = 100;
divHeight = 100;
divId = 'divLogin'; // id of the div that you want to show in center
// Get the x and y coordinates of the center in output browser's window
var centerX, centerY;
if (self.innerHeight)
{
centerX = self.innerWidth;
centerY = self.innerHeight;
}
else if (document.documentElement && document.documentElement.clientHeight)
{
centerX = document.documentElement.clientWidth;
centerY = document.documentElement.clientHeight;
}
else if (document.body)
{
centerX = document.body.clientWidth;
centerY = document.body.clientHeight;
}
var offsetLeft = (centerX - divWidth) / 2;
var offsetTop = (centerY - divHeight) / 2;
// The initial width and height of the div can be set in the
// style sheet with display:none; divid is passed as an argument to // the function
var ojbDiv = document.getElementById(divId);
ojbDiv.style.position = 'absolute';
ojbDiv.style.top = offsetTop + 'px';
ojbDiv.style.left = offsetLeft + 'px';
ojbDiv.style.display = "block";
}
catch (e) {}
}
You can then call the function through any event, for example:
<body onload='ShowDivInCenter();' onresize='ShowDivInCenter();'>
if you want it to be dynamic.

Angular nested directive ordering

i am having a hard time finding any information on the ordering of directives and their updating of css properties.
for example, i have two directives, one to set an element to full screen height, and one to align content vertically.
app.directive('fullscreenElement', function() {
return {
restrict: "A",
link: function(scope,element,attrs){
$(element).each(function(){
$(this).css('height', $(window).height());
});
}
};
});
app.directive('alignVertical', function() {
return {
restrict: "A",
link: function(scope,element,attrs){
var height = $(element).height();
var parentHeight = $(element).parent().height();
var padAmount = (parentHeight / 2) - (height / 2);
$(element).css('padding-top', padAmount);
}
};
});
They both work independantly, the trouble is when they are nested, the align-vertical directive doesnt work, im assuming this is because the css height hasn't been set yet? how do i make sure it is set before the alignVertical directive runs? any tips for writing these two directives in a more angular way would be appreciated.
this works:
<header style="height:800px">
<div align-vertical>
this content is centered vertically as expected
</div>
</header>
this doesn't work (content doesnt center vertically, even though header height is now fullscreen):
<header fullscreen-element>
<div align-vertical>
the header element is now fullscreen height but this content is *not* centered vertically
</div>
</header>
thanks
Figured out a solution, posting it here in case anyone finds it helpful.
The trick is to use scope.watch and scope.evalAsync to monitor changes of height to the parent container and run them after rendering is complete.
app.directive('alignVertical', function() {
return {
link: function($scope, element, attrs) {
// Trigger when parent element height changes changes
var watch = $scope.$watch(function() {
return element.parent().height;
}, function() {
// wait for templates to render
$scope.$evalAsync(function() {
// directive runs here after render.
var that = $(element);
var height = that.height();
var parentHeight = that.parent().height();
var padAmount = (parentHeight / 2) - (height / 2);
that.css('padding-top', padAmount);
});
});
},
};
});

Resources