In AngularJS 1.2, if I use a parent animation, the child animation doesn't work.
If I comment out app.animation('.parent', function () { .. }, then the child animation starts correctly.
How to get both parent and child animations working at the same time?
Plunker of my code
HTML:
<button ng-click="anim.toggleParent()">reveal parent</button>
<button ng-click="anim.toggleChild()">reveal child</button>
<div class="parent" ng-if="!anim.showParent">
<div class="child" ng-if="!anim.showChild">
</div>
</div>
JS:
app.animation('.parent', function () {
return {
// ...
};
});
// this doesn't work with parent animation =(
app.animation('.child', function () {
return {
// ...
};
});
Just insert ng-animate-children to the parent (Angular 1.2+).
<button ng-click="anim.toggleParent()">reveal parent</button>
<button ng-click="anim.toggleChild()">reveal child</button>
<div class="parent" ng-if="!anim.showParent" ng-animate-children>
<div class="child" ng-if="!anim.showChild">
</div>
</div>
Check the ngAnimate documentation:
Keep in mind that, by default, if an animation is running, any child elements cannot be animated until the parent element's animation has completed. This blocking feature can be overridden by placing the ng-animate-children attribute on a parent container tag.
<div class="slide-animation" ng-if="on" ng-animate-children>
<div class="fade-animation" ng-if="on">
<div class="explode-animation" ng-if="on">
...
</div>
</div>
</div>When the on expression value changes and an animation is triggered then each of the elements within will all animate without the block being applied to child elements.
Not sure whether this thread is closed. If so recommendation would be very helpful.
Facing the same issue here.
Angular animate has the below lines indicating that the child animations will not be triggered if parent has animation.
Not sure whether this is an issue or works as expected.
//skip the animation if animations are disabled, a parent is already being animated,
//the element is not currently attached to the document body or then completely close
//the animation if any matching animations are not found at all.
//NOTE: IE8 + IE9 should close properly (run closeAnimation()) in case a NO animation is not found.
if (animationsDisabled(element, parentElement) || matches.length === 0) {
domOperation();
closeAnimation();
return;
}
Have raised a thread in Angular google group referenced the issue back here.
Also not sure if this thread is closed, but you could always edit the angular-animate.js file. Function animationsDisabled is where angular looks for the parent element to see if it will allow the child to animate. At the top of this function I added a check to see if the parent element has a class of animation-override (can be whatever you define). This way you can override the default functionality when needed.
function animationsDisabled(element, parentElement) {
if (parentElement[0].classList.contains('animation-override')) return false;
if (rootAnimateState.disabled) return true;
if(isMatchingElement(element, $rootElement)) {
return rootAnimateState.disabled || rootAnimateState.running;
}
do {
//the element did not reach the root element which means that it
//is not apart of the DOM. Therefore there is no reason to do
//any animations on it
if(parentElement.length === 0) break;
var isRoot = isMatchingElement(parentElement, $rootElement);
var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
var result = state && (!!state.disabled || !!state.running);
if(isRoot || result) {
return result;
}
if(isRoot) return true;
}
while(parentElement = parentElement.parent());
return true;
}
}]);
Related
In my angular UI code, I have a component class that calls a like below
app.component.html
//...
<div class="banner">
<p-dialog [(visible)]="displayCOI" styleClass="coiDialog" [contentStyle]="{'overflow-y': 'hidden'}" [modal]="true" [style]="{width: '75vw'}" [baseZIndex]="10000" [showHeader]="false"
[draggable]="false" [resizable]="false">
<coi (notify)="onCoIAccept($event)"></coi>
</p-dialog>
</div>
...///
coi.component.html looks like below
<div>
<div class="row" style="padding: 10px 0px">
<div class="col-sm-4">
<p align="center"><b>Instructions</b></p>
<br>...//
</div>
<div #scrollDiv id="scrollDiv" class="col-sm-6" style="height:350px; overflow-y: scroll;" (scroll)="onScroll($event)">
<p-table #dt [columns]="cols" [scrollable]="true" [value]="usersLi" [(selection)]="selectedUsersLi" dataKey="id">
//....
..///
</p-table>
</div>
<div class="col-sm-2">
<div align="center">
<button pButton type="button" label="Accept" [disabled]="disableAccept" (click)="close()" class="ui-button-rounded"></button>
</a>
</div>
</dv>
</div>
coi.component.ts code is as below:
export class coiComponent {
#ViewChild("scrollDiv") scrollDiv: ElementRef;
disableAccept: boolean = false;
ngOnInit():void {
this.keys = Object.keys(this.propertyObj);
this._utilService.convertKeysToHeader(this.keys,this.cols);
this.disableAccept = true;
this.loadRecords();
}
onScroll(event: any) {
// visible height + pixel scrolled >= total height
if (event.target.offsetHeight + event.target.scrollTop >= event.target.scrollHeight) {
this.disableAccept = false;
console.log("End");
}
}
ngOnChanges(changes: SimpleChanges){
console.log("ngOnChanges" , changes);
for ( const propName in changes){
let change = changes[propName];
if ( propName == 'coi'){
// console.log('CHANGED...DO HERE');
console.log(this.scrollDiv.nativeElement.offsetHeight);
console.log(this.scrollDiv.nativeElement.scrollHeight);
}
}
}
}
As you can see the modal is divided into 3 DIV. 1. instrucations, 2: table, 3. Accept button
The modal by itself has a fixed height and scroll hidden. The div with table has a fixed height and overflow scroll and it works perfectly. Now the table can be with 30-50 records so vertical scrolling is enabled. I want the accept button on the 3rd div to be enabled only when the user had scrolled the table and has seen all the records. So the function (scroll)="onScroll($event)" enables only when the scroll is scrolled completely and it works perfectly.
my question is Some users may see less than 5-10 records which means scroll wouldn't be enabled for those users and accept also need to be enabled for them. Any suggestion on how to do this, please? I tried adding an id for the div tag called "scrollDiv" and #scrollDiv and passing this as an ElementRef and on ngOnChange trying to get the offsetHeight and scrollHeight but I get value '0' on all the cases.` Can someone help me with this?
I have updated my question. Please give some suggestions. Thank you.
I'll try to give you a running idea, then you can understand and apply it to your case. You can check at this link.
Explanation of the example
In the component you have 4 important things to consider:
isActionDisabled a variable that says if your action should be disabled or not
#ViewChild('containerElement') containerElement a refer to the scrollable container of the table
onScrollContainer a method that's executed when you scroll the containerElement
disableByScroll a method that changes the value of isActionDisabled according to the position of the scrollbar of the containerElement. If it's on bottom isActionDisabled is false otherwise is true.
The most important method is disableByScroll:
disableByScroll(): void {
if (this.containerElement) {
const element = this.containerElement.nativeElement;
this.isActionDisabled = !(
element.scrollTop ===
element.scrollHeight - element.clientHeight
);
} else {
this.isActionDisabled = true;
}
}
Please read this article to understand what I did.
disableByScroll is called each time a scroll event is called on containerElement
onScrollContainer(): void {
this.disableByScroll();
}
and after view init
ngAfterViewInit(): void {
this.disableByScroll();
this.cdr.detectChanges();
}
That is useful if you have a number of items that do not activate the scrollbar. Please, read this guide to understand the lifecycle events of an Angular application. As you can see I called a method detectChanges of the ChangeDetectorRef. Reading that guide you'll understand why.
About the template, it's pretty simple and you can figure it out.
I've got an html element with overflow: auto, but I want to give it a border only when it's scrollable.
How do I evaluate the element's size from within ngClass without getting any kind of null errors?
Note: The element's enclosing div doesn't get rendered until after getting a response from an observable.
Attempt 1:
The html element is set up like this:
<div ngIf="!loading">
<div id="{{someID}}" [ngClass]="{'border-class': isScrollable}"> ... </div>
</div>
In my ngOnInit, I call a function to see if the given element can be scrolled.
ngOnInit() {
// this.loading gets set to false after an observable is returned
/* ... */
// scroll check
let e = document.getElementById(`${this.someID}`);
if (element !== null) {
this.isScrollable = e.scrollHeight > e.clientHeight;
}
}
If I don't check for null, I get errors. If I do check for null, then even if I have scrollable content, when the page is loaded, the border doesn't show up.
I thought the issue might be with this.loading, so I added the scroll check within the observable response, but after loading was set to false. Still no border.
Attempt 2:
<div #textDiv [ngClass]="{'border-class': isScrollable}"> ... </div>
#ViewChild('textDiv') element: ElementRef;
/* ... */
ngAfterViewInit() {
this.isScrollable = this.element.scrollHeight > this.element.clientHeight;
}
But the border still doesn't show up on scrollable content when the page is loaded.
Attempt 3:
The only thing that has worked, is this hot mess:
setTimeout( () => {
this.isScrollable = this.element.scrollHeight > this.element.clientHeight;
});
Is there a way I can get this to work without calling setTimeout?
The problem is here:
<div ngIf="!loading">
<div id="{{someID}}" [ngClass]="{'border-class': isScrollable}"> ...
</div>
The isScrollable is not updating the value after it changes.
You can fix this, using a get and returning the value when ngAfterViewInit was already executed:
export class CustomComponent implements AfterViewInit {
private afterViewInitExecuted = false;
#ViewChild('textDiv') element: ElementRef;
public get isScrollable() {
if(this.afterViewInitExecuted) {
return this.element.scrollHeight > this.element.clientHeight;
}
return false;
}
ngAfterViewInit() {
this.afterViewInitExecuted = true;
}
}
Then in your html:
<div ngIf="!loading">
<div id="{{someID}}" [ngClass]="{'border-class': isScrollable() }"> ...
</div>
With that, it should work.
I'm using Redux in my app, inside a Component I want to scroll to an specific div tag when a change in the store happens.
I have the Redux part working so it triggers the componentDidUpdate() method (I routed to this compoennt view already).
The problem as far as I can tell, is that the method scrollIntoView() doesn't work properly cos componentDidUpdate() has a default behavior that scrolls to the top overwriting the scrollIntoView().
To work-around it I wrapped the function calling scrollIntoView() in a setTimeout to ensure that happens afeterwards.
What I would like to do is to call a preventDefault() or any other more elegant solution but I can't find where to get the event triggering the 'scrollTop'
I looked through the Doc here: https://facebook.github.io/react/docs/react-component.html#componentdidupdate
and the params passed in this function are componentDidUpdate(prevProps, prevState) ,since there is no event I don't know how to call preventDefault()
I've followd this Docs: https://facebook.github.io/react/docs/refs-and-the-dom.html
And tried different approaches people suggested here: How can I scroll a div to be visible in ReactJS?
Nothing worked though
Here is my code if anyone has any tip for me, thanks
class PhotoContainer extends React.Component {
componentDidUpdate(){
setTimeout(() => {
this.focusDiv();
}, 500);
}
focusDiv(){
var scrolling = this.theDiv;
scrolling.scrollIntoView();
}
render() {
const totalList = [];
for(let i = 0; i < 300; i += 1) {
totalList.push(
<div key={i}>{`hello ${i}`}</div>
);
}
return (
<div >
{totalList}
<div ref={(el) => this.theDiv = el}>this is the div I'm trying to scroll to</div>
</div>
)
};
}
Ok it's been a while but I got it working in another project without the setTimeOut function so I wanted to answer this question.
Since Redux pass the new updates through props, I used the componentWillRecieveProps() method instead of componentDidUpdate() , this allowes you a better control over the updated properties and works as expected with the scrollIntoView() function.
class PhotoContainer extends React.Component {
componentWillReceiveProps(newProps) {
if (
this.props.navigation.sectionSelected !==
newProps.navigation.sectionSelected &&
newProps.navigation.sectionSelected !== ""
) {
this.focusDiv(newProps.navigation.sectionSelected);
}
}
focusDiv(section){
var scrolling = this[section]; //section would be 'theDiv' in this example
scrolling.scrollIntoView({ block: "start", behavior: "smooth" });//corrected typo
}
render() {
const totalList = [];
for(let i = 0; i < 300; i += 1) {
totalList.push(
<div key={i}>{`hello ${i}`}</div>
);
}
return (
<div >
{totalList}
<div ref={(el) => this.theDiv = el}>
this is the div I am trying to scroll to
</div>
</div>
)
};
}
I also struggled with scrolling to the bottom of a list in react that's responding to a change in a redux store and I happened upon this and a few other stackoverflow articles related to scrolling. In case you also land on this question as well there are a few ways this could be a problem. My scenario was that I wanted a 'loading' spinner screen while the list was rendering. Here are a few wrong ways to do this:
When loading = true, render spinner, otherwise render list.
{loading ?
<Spinner />
:
<List />
}
as stated above this doesn't work because the list you might want to scroll to the bottom of isn't rendered yet.
When loading set the display to block for the spinner and none for the list. When done loading, reverse the display.
<div style={{display: loading ? 'block' : 'none'>
<Spinner />
</div>
<div style={{display: loading ? 'none' : 'block'>
<List />
</div>
This doesn't work either since the list you want to scroll to the bottom of isn't actually being displayed likely when you call the scroll.
The better approach for the above scenario is to use a loading that acts as an overlay to the component. This way both the spinner and list are rendered and displayed, the scroll happens, and when the loading is complete, the spinner can be de-rendered or set to be invisible.
var mark = null;
class Demo extends React.Component {
handleClick(evt) {
mark = "outer";
}
handleSpanClick(evt) {
mark = "inner";
}
render() {
return (
<div onClick={this.handleClick.bind(this)}>
<span onClick={this.handleSpanClick.bind(this)}>
inner
</span>
</div>
)
}
}
For example, I expect that the mark will be "inner" when I click span, but actually, the mark will be "outer". I know the onClick event of span will be called firstly, so I cant get "inner".
How can I get "inner" in this sample?
Example for Bubbling and Capturing in React.js
Bubbling and capturing are both supported by React in the same way as
described by the DOM spec, except for how you go about attaching
handlers.
<div onClickCapture={this.handleClick.bind(this)}>
...
All,
How can I pass the current DOM element to the Angular directive "ng-disabled"?
I do know that messing w/ the DOM in Angular is bad practice. But I can't think of another - simple - way to do this. Here is my problem:
I have a button that updates a scope variable when clicked:
<button ng-click="target_model.display_detail=true">click me</button>
Elsewhere in my template there is code that watches "target_model.display_detail" - when it is true it displays a modal-dialog which includes an Angular directive which gets some data from the server and populates a form which includes another button like the one above.
The data structure that I am working w/ is potentially recursive; there are loads of nested "target_models". So it is possible for a button in a modal-dialog to point a target_model whose form has already been created. In that case, I just want to disable the button. I'd like todo something like:
<button ng-disabled="ancestor_model_exists(the_current_element, target_model.some_unique_id)">click me</button>
Where "ancestor_model_exists" is a function that would check the DOM to see if there is an ancestor element with a particular id. But how do I know which element to start from?
You're approaching DOM manipulations imperatively - the jQuery way, not declaratively - the Angular way.
DOM manipulation is fine... inside directives. You don't do it in controllers, where you presumably defined that function.
When you get a chance, try to get away with 0 calls to $ in a sandbox to force you to learn how to do things the Angular way - not because it's "better" in an absolute way - it's just generally better to first learn the toolkit and recommended approaches before doing it your way, anyway.
This should do what you want, except maybe searching beyond multiple ancestors (but I mention how to do that if you need that):
https://plnkr.co/edit/7O8UDuqsVTlH8r2GoxQu?p=preview
JS
app.directive('ancestorId', function() {
return {
restrict: 'A',
controller: 'AncestorIdController',
require: ['ancestorId'],
link: function(scope, element, attrs, controllers) {
var ancestorIdController = controllers[0];
// If you wanted to use an expression instead of an
// interpolation you could define an isolate scope on this
// directive and $watch it.
attrs.$observe('ancestorId', function(value) {
ancestorIdController.setId(value);
});
}
}
});
app.controller('AncestorIdController', function() {
this.getId = _getId;
this.setId = _setId;
var id;
function _getId() {
return id;
}
function _setId(value) {
id = value;
}
});
app.directive('disableForAncestorId', function() {
return {
restrict: 'A',
require: ['?^ancestorId'],
link: function(scope, element, attrs, controllers) {
var ancestorIdController = controllers[0];
// Check to make sure the ancestorId is a parent.
if (ancestorIdController) {
scope.$watch(function() {
var watch = {
target: ancestorIdController.getId(),
actual: attrs.disableForAncestorId
};
return watch;
}, function(value) {
if (value.target === value.actual) {
element.attr('disabled', 'disabled');
} else {
element.removeAttr('disabled');
}
}, true /* Deep watch */ );
}
}
}
});
HTML
<!-- The simple happy path. -->
<div ancestor-id="A">
<button disable-for-ancestor-id="A">'A' === 'A' ?</button>
</div>
<!-- require will match the 'B' before the 'A' because it's closer.
if you need to match any parent you could use a third coordinating
directive. -->
<div ancestor-id="A">
<div ancestor-id="B">
<button disable-for-ancestor-id="A">'B' === 'A' ?</button>
</div>
</div>
<!-- require lets you freely change the DOM to add extra elements separating
you from what you're looking for.-->
<div ancestor-id="B">
<div>
<div>
<button disable-for-ancestor-id="B">'B' === 'B' ?</button>
</div>
</div>
</div>
<!-- It doesn't blow up if it doesn't find an ancestorId. -->
<div>
<button disable-for-ancestor-id="B">'B' === undefined ?</button>
</div>
<br>
Dynamic AncestorId test (it will be disabled if the text fields are equal):
<br>
Target AncestorId <input ng-model="targetAncestorId">
<br>
Actual Ancestor <input ng-model="actualAncestorId">
<!-- It doesn't blow up if it doesn't find an ancestorId. -->
<div ancestor-id="{{ targetAncestorId }}">
<button disable-for-ancestor-id="{{ actualAncestorId }}">'{{ actualAncestorId }}' === '{{ actualAncestorId }}' ?</button>
</div>
It never fails... posting a question on Stack Overflow always makes me realize the answer just minutes later.
The following code gets me pretty close:
template.html:
<button ng-click="display_if_ancestor_model_exists($event, target_model)">click me</button>
app.js:
$scope.display_if_ancestor_model_exists = function($event, target_model) {
var ancestor_form = $($event.target).closest("#" + target_model.some_unique_id);
if (ancestor_form.length) {
/* don't show the modal-dialog */
display_msg("This form already exists!");
}
else {
target_model.display_detail = true;
}
};
Of course, I would rather the button just be disabled but I can live w/ this solution for now.