I have a div which has data coming in remotely from a subscribed service
overflow-y is set to scroll
I have to put custom arrows when the overflow happens and user can click, and I have a scroll method with a #templateRef which will handle manual scrolling
template and ts look like below
#ViewChild('scrollContent') scrollContent: ElementRef
ptivate data
private overflow: boolean = false
constructor (private service: DataService, private cdr: ChangeDetectorRef) {}
ngOnInit () {
this.service.subscribe(data => {
this.data = data
this.isOverflow()
})
isOverFlow () {
if (this.scrollContent.nativeElement.scrollHeight > this.scrollContent.nativeElement.clientHeight) {
this.overflow = true
} else {
this.overflow = false
}
}
}
<div *ngIf="overflow" (click)="scroll($event,'up')">
<div class="uparrow">
</div>
</div>
<div class="content" #scrollContent>
<!--
Iterate or display data thus changing the content of div and might cause
overflow-y
-->
</div>
<div *ngIf="overflow" (click)="scroll($event,'down')">
<div class="downarrow">
</div>
</div>
But isOverflow() returns false and console.log runs before and prints false before the view is updated
I tried some changeDetectionMethods like cdr.detectChanges() and markForChange() did not have any luck with it.
I can manually trigger a setTimeOut and that might work but I am looking for angular specific answer
P.S. I have tried some life-cycle-hooks too but obviously you don't want them to trigger like 1000 times a second so I have avoided using some of them unless I am missing one which solves this purpose and gets called exactly once for example ngAfterViewChecked() is not performant at all as it keeps getting called again and again
Angular version is 5.2.10
Thanks
Related
Say, we have a Stimulus controller that toggles a sidebar. The button that triggers the toggle action is located in different places, though, depending on the device. E.g. in the header when you are on a mobile device and in the main navigation when you are on a Desktop device.
What do you do in this situation? Is it better to initialise 2 Stimulus controllers (one in the div that belongs to the header and one that belongs to the main navigation) or to initialise just one Stimulus controller, for example in a wrapper div tag that encloses both the header as well as the main navigation?
I would put a sidebar controller on the body tag.
As long as the sidebar controller is not reused it's totally fine. It wouldn't work for a generic toggler controller.
<body data-controller="sidebar">
<header class="sm:hidden">
<button data-action="sidebar#toggle">Toggle sidebar</button>
</header>
<section data-sidebar-target="sidebar">
Some content in the sidebar
</section>
<main>
<button class="hidden sm:inline-block" data-action="sidebar#toggle">Toggle sidebar</button>
</main>
</body>
I would recommend avoiding controllers with too much scope where practical. Having a large section of the DOM with multiple controllers on it could risk some performance issues.
An alternative approach to having a sidebar & sidebar-toggle controller is to have a generic 'fire an event' controller.
We can even take inspiration from Alpine.js and its x-on directive. We could have a smaller scoped 'on' action do something type controller that let's us have a generic way to trigger any event.
HTML
Let's start with the HTML, similar to the previous answer, we have a sidebar div and then two buttons that do basically the same thing (trigger an event).
We can leverage Stimulus' approach to coordinating with DOM events to help us here but this time with a custom global event with the name 'sidebar:toggle'.
Note that the sidebar controller listens to the event with #window to ensure that it picks up any 'bubbling' events all the way up the DOM.
https://stimulus.hotwired.dev/reference/controllers#cross-controller-coordination-with-events
<body>
<header class="sm:hidden">
<button
data-controller="on"
data-action="click->on#go"
data-on-event-name-param="sidebar:toggle"
>
Toggle sidebar
</button>
</header>
<section data-controller="sidebar" data-action="sidebar-toggle#window->sidebar#layout">
Some content in the sidebar
</section>
<main>
<button
class="hidden sm:inline-block"
data-controller="on"
data-action="click->on#go"
data-on-event-name-param="sidebar:toggle"
>
Toggle sidebar
</button>
</main>
</body>
JavaScript (Controllers)
The Sidebar controller will have whatever you want but also a method for toggle that we can trigger with the action.
import { Controller } from '#hotwired/stimulus';
class Sidebar extends Controller {
static targets = [];
static values = { expanded: { default: false, type: Boolean } };
connect() {
// do the things
}
toggle() {
this.expandedValue = !this.expandedValue;
}
}
The On controller leverages Stimulus action parameters to be able to receive any dynamic value set on the DOM as it's event.
Note that the On controller intentionally makes the events bubble (this is the default, but good to be explicit) so that any other event listeners on window can listen to this.
import { Controller } from '#hotwired/stimulus';
class On extends Controller {
go({ params: { eventName } }) {
this.dispatch(eventName, { prefix: '', bubbles: true, cancelable: false });
}
}
I have been trying this for more than a week but have not been able to achieve it in angular. Can someone please take a look into it?
onContainerScroll() never gets called, I was wondering how to do it using javascript, on scroll this element and on reaching bottom call API with next range?
<ngx-monaco-diff-editor id="diffeditor" [options]="diffOptions" [originalModel]="originalModel" #elm
[modifiedModel]="modifiedModel" (scroll)="onContainerScroll($event)">
</ngx-monaco-diff-editor>
#ViewChild('elm', { read: ElementRef }) elm: ElementRef;
ngAfterViewInit() {
console.log(this.elm.nativeElement) //return the ngx element
}
You want to get the reference to the DiffEditorComponent and then
<ngx-monaco-diff-editor
id="diffeditor"
[options]="diffOptions"
[originalModel]="originalModel"
[modifiedModel]="modifiedModel">
</ngx-monaco-diff-editor>
#ViewChild(DiffEditorComponent) diffEditor: DiffEditorComponent;
ngAfterViewInit() {
console.log(diffEditor._editorContainer); // HTMLElement that might be scrolled
}
You will have to dig deeper, but this is a reasonable start to go forth.
I have an ionic project that is using dragula, but I'm having an issue setting the mirrorContainer. I'd like to make the container something other than the default body because I believe that's what is attributing to a strange scrolling problem I'm having while dragging.
I've created my bag in html
<div class="step-container--line" [dragula]='"bag"' id="mirror">
<div class="card">
....
</div>
Then in the JS, I've initialized dragula in the constructor and started to set its options.
constructor(private dragulaService: DragulaService) {
dragulaService.setOptions('bag', {
moves: function (el, container, handle) {
return handle.className === 'step__menu__button';
},
direction: 'vertical',
//mirrorContainer: document.getElementById('mirror')
});
dragulaService.drag.subscribe((value) => {
this.onDrag(value.slice(1));
});
dragulaService.drop.subscribe((value) => {
this.onDrop(value.slice(1));
});
}
The problem is; when I add mirrorContainer: document.getElementById('mirror') to the setOptions, my mirror container comes back as null. I'm assuming because this loads before the DOM does and there's no instance of #mirror yet.
If I moved everything down into ionViewDidLoad(){}, I get an error stating that the bag 'bag' already exists.
I'm not sure the best way to initialize or add to the options after the DOM loads. Any ideas?
I know I'm late to the party but I had the same issue using Angular 11.
After your element is in the DOM and is available you can do this.
ionViewDidLoad() {
(this.dragulaService as any).groups['bag'].options.mirrorContainer = document.getElementById('mirror');
}
I have done some reading and investigation on this error, but not sure what the correct answer is for my situation. I understand that in dev mode, change detection runs twice, but I am reluctant to use enableProdMode() to mask the issue.
Here is a simple example where the number of cells in the table should increase as the width of the div expands. (Note that the width of the div is not a function of just the screen width, so #Media cannot easily be applied)
My HTML looks as follows (widget.template.html):
<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
<td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>
This on its own does nothing. I'm guessing this is because nothing is causing change detection to occur. However, when I change the first line to the following, and create an empty function to receive the call, it starts working, but occasionally I get the 'Expression has changed after it was checked error'
<div #widgetParentDiv class="Content">
gets replaced with
<div #widgetParentDiv (window:resize)=parentResize(10) class="Content">
My best guess is that with this modification, change detection is triggered and everything starts responding, however, when the width changes rapidly the exception is thrown because the previous iteration of change detection took longer to complete than changing the width of the div.
Is there a better approach to triggering the change detection?
Should I be capturing the resize event through a function to ensure
change detection occurs?
Is using #widthParentDiv to access the
width of the div acceptable?
Is there a better overall solution?
For more details on my project please see this similar question.
Thanks
To solve your issue, you simply need to get and store the size of the div in a component property after each resize event, and use that property in the template. This way, the value will stay constant when the 2nd round of change detection runs in dev mode.
I also recommend using #HostListener rather than adding (window:resize) to your template. We'll use #ViewChild to get a reference to the div. And we'll use lifecycle hook ngAfterViewInit() to set the initial value.
import {Component, ViewChild, HostListener} from '#angular/core';
#Component({
selector: 'my-app',
template: `<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
<td>Value1</td>
<td *ngIf="divWidth > 350">Value2</td>
<td *ngIf="divWidth > 700">Value3</td>
</tr>
</table>`,
})
export class AppComponent {
divWidth = 0;
#ViewChild('widgetParentDiv') parentDiv:ElementRef;
#HostListener('window:resize') onResize() {
// guard against resize before view is rendered
if(this.parentDiv) {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
ngAfterViewInit() {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
}
}
Too bad that doesn't work. We get
Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'.
The error is complaining about our NgIf expressions -- the first time it runs, divWidth is 0, then ngAfterViewInit() runs and changes the value to something other than 0, then the 2nd round of change detection runs (in dev mode). Thankfully, there is an easy/known solution, and this is a one-time only issue, not a continuing issue like in the OP:
ngAfterViewInit() {
// wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
}
Note that this technique, of waiting one tick is documented here: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child
Often, in ngAfterViewInit() and ngAfterViewChecked() we'll need to employ the setTimeout() trick because these methods are called after the component's view is composed.
Here's a working plunker.
We can make this better. I think we should throttle the resize events such that Angular change detection only runs, say, every 100-250ms, rather then every time a resize event occurs. This should prevent the app from getting sluggish when the user is resizing the window, because right now, every resize event causes change detection to run (twice in dev mode). You can verify this by adding the following method to the previous plunker:
ngDoCheck() {
console.log('change detection');
}
Observables can easily throttle events, so instead of using #HostListener to bind to the resize event, we'll create an observable:
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );
This works, but... while experimenting with that, I discovered something very interesting... even though we throttle the resize event, Angular change detection still runs every time there is a resize event. I.e., the throttling does not affect how often change detection runs. (Tobias Bosch confirmed this:
https://github.com/angular/angular/issues/1773#issuecomment-102078250.)
I only want change detection to run if the event passes the throttle time. And I only need change detection to run on this component. The solution is to create the observable outside the Angular zone, then manually call change detection inside the subscription callback:
constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
// set initial value, but wait a tick to avoid one-time devMode
// unidirectional-data-flow-violation error
setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
this.ngzone.runOutsideAngular( () =>
Observable.fromEvent(window, 'resize')
.throttleTime(200)
.subscribe(_ => {
this.divWidth = this.parentDiv.nativeElement.clientWidth;
this.cdref.detectChanges();
})
);
}
Here's a working plunker.
In the plunker I added a counter that I increment every change detection cycle using lifecycle hook ngDoCheck(). You can see that this method is not being called – the counter value does not change on resize events.
detectChanges() will run change detection on this component and its children. If you would rather run change detection from the root component (i.e., run a full change detection check) then use ApplicationRef.tick() instead (this is commented out in the plunker). Note that tick() will cause ngDoCheck() to be called.
This is a great question. I spent a lot of time trying out different solutions and I learned a lot. Thank you for posting this question.
Other way that i used to resolve this:
import { Component, ChangeDetectorRef } from '#angular/core';
#Component({
selector: 'your-seelctor',
template: 'your-template',
})
export class YourComponent{
constructor(public cdRef:ChangeDetectorRef) { }
ngAfterViewInit() {
this.cdRef.detectChanges();
}
}
Simply use
setTimeout(() => {
//Your expression to change if state
});
The best solution is to use setTimeout or delay on the services.
https://blog.angular-university.io/angular-debugging/
Mark Rajcok gave a great answer. The simpler version (without throttling) would be:
ngAfterViewInit(): void {
this.windowResizeSubscription = fromEvent(window, 'resize').subscribe(() => this.onResize())
this.onResize() // to initialize before any change
}
onResize() {
this.width = this.elementRef.nativeElement.getBoundingClientRect().width;
this.changeDetector.detectChanges();
}
I have the below HTML that displays a spinner until a list of tags displays for an application. However, the spinner continues if there are no events that exist either.
I'd like the spinner to disappear and the input just be blank or return a message to the user that there are no tags for this app. Not sure how to do that whilst keeping the code clean!
HTML:
<div ng-show="noun === 'Tag'">
<div class="spinner" ng-hide="loadedTags()">
<i class="fa fa-spinner fa-pulse"></i>
</div>
<div ng-show="loadedTags()">
<select ol-filter-select="tags" ng-model="tagName">
<option ng-value="name" ng-repeat="name in tagNames()" ng-bind="name" ng-selected="tagName === name">
</option>
</select>
</div>
</div>
Controller:
$scope.loadedTags = function() {
return !_.isEmpty($scope.tags);
};
View:
Spinner keeps spinning if there's no tags...
I think you need to show more of the code that is actually loading the tags. Are you loading the tags from the server via $http.get or something similar? If so, then typically the way this would be done is to have a boolean scope variable that specifically represents the fact that loading is currently in progress. That variable would control whether the spinner is visible. For example, I have common code that looks like this:
Constructor for Angular controller:
$scope.loadingInProgress = true;
$http.get(myUrl)
.then(function(result) {
// My success function
$scope.tags = result.data;
$scope.loadingInProgress = false;
},
function() {
// My failure function
$scope.loadingInProgress = false;
});
HTML:
<div class="spinner" ng-show="loadingInProgress">
<i class="fa fa-spinner fa-pulse"></i>
</div>
It seems like you're trying to avoid an extra scope variable to represent the loading state, so you're trying to calculate the loading state based on the results of the load. But that's not really reliable. The results of the load could be anything, and doesn't really indicate whether loading is in progress.
The above should work, assuming you're using underscore.js. You're sure the controller is in the same scope as that bit of html?
Maybe try the following:
$scope.loadedTags = function() {
return $scope.tags.length > 0;
};
loadedTags() will be evaluated only on digest cycle, not every time $scope.tags is changed.
Instead, create variable $scope.isVisible watch on $scope.tags to update isVisible state.
Then, your ng-show=isVisible.