Angular sub-component style does not change until next ngAfterViewChecked - javascript

Angular version: 5.0.3
I have put my code here.
I have made a custom inkbar module, and imported it to the app. I use
<lib-inkbar [nextEle]="inkbarSubject" [color]="inkbarColor"></lib-inkbar>
...to install it in my app.component.
The inkbar under the navigation menu tabs is expected to automatically go in position where router link is currently active. (And this is done by getting the width and offset left of a given element, and move to it.) For example, at the beginning, "/index" is active, so "Index" tab has a class "active", and inkbar should translate under it.
I have achieved this by putting the code in Angular's ngAfterViewChecked life-cycle. inkbarMove() tells inkbar where to go by telling it the element (i.e. the active nav tab).
ngAfterViewChecked() {
this.navMenus = this.ul.nativeElement.children;
let actMenu = this.getAvtiveMenu();
if (actMenu) {
this.inkbarMove(actMenu);
}
}
inkbarMove = (ele: HTMLBaseElement | MouseEvent) => {
if (ele instanceof MouseEvent) {
ele = ele.target as HTMLBaseElement;
}
this.inkbarSubject.next(ele);
};
However, this works only after next ngAfterViewChecked() fires. When page first loads, the inkbar will stay {width: 0px; left: 0px} style, as initial default value, no matter how many times I tell it to "Move" (I even wrote a for-loop, which made my browser crash, but still didn't work lol). It will only starts to move after I press the button and manually call ngAfterViewChecked() again.
Does someone know what is going on here?

Related

React-flow & dare: reactFlowInstance.fitView() fits the instance in the screen after 2nd button click. (1st only changes the direction of the graph)

I have tried different ways of implementing this beauty, but it doesn't seem to work. My problem is that when I hit the button, I want to change the layout of the graph, which happens and I am glad for it, but I also want my graph to be centered (fit) on the screen. The first button click changes the direction, but doesn't fit the instance. To fit the instance I need to hit the button for a second time. I guess it has to do with asynchronous JS or? This is my code:
const onChangeTreeLayout = useCallback(
(treeLayoutDirection) => {
const layoutedElements = getLayoutedGraphElements(
elements,
treeLayoutDirection,
setTreeLayoutDirection
);
setElements(layoutedElements);
},
[elements]
);
Then how I get the instance and trigger it follows: Note: I can't use useReactFlow() hook as we decided not to migrate to a newer version. But useZoomPanHelper does its work anyway.
const reactFlowInstance = useZoomPanHelper();
<button
onClick={() => {
onChangeTreeLayout('TB');
reactFlowInstance.fitView();
}}
>
Horizontal Layout
</button>
I have also tried putting the function .fitView() inside the onChangeTreeLayout but I get the same behaviour.

gatsby setPageSection makes page jump after images finish loading

This current piece of code:
<button onClick={() => setPageSection([p.slug+"/", s.slug])}>
makes my page jump to the start of the section when the images finish loading
In this case I have a grid of pictures that are correctly lazy loaded with an initial base64 lowres pre-render but everytime each one of the pictures finishes loading the page scrolls back to the top of the section.
This only happens when I use the setPageSection.
When I click a direct link to an anchor ("page/#section") of the page instead of using the above method of setting the page section the correct behavior happens (images continue lazy loading in the background and the pages doesn't snap to the top of the section everytime when each one finishes loading).
UPDATE:
The following looks like the culprit:
enter useEffect(() => {
if (typeof prevPageSection === 'undefined') return;
const scrollToSection = () => {
const el = document.getElementById(pageSection[1]);
if (scrollContainer.current && el) {
scrollContainer.current.scrollTo({ top: el.offsetTop });
}
};
if (pageSection[0] !== prevPageSection![0] && pageSection[0] !== pageContext.slug) {
navigate(locale + pageSection[0]);
scrollContainer.current?.addEventListener('load', scrollToSection, true);
}
scrollContainer.current?.removeEventListener('load', scrollToSection);
}, [pageSection, prevPageSection, locale, pageContext.slug]);
If I remove these event listeners the page stops jumping after the images load but the links get broken (they become useless when navigating to a section on a different page, without these event listeners one can only jump to the desired section if already on that page, otherwise the browser will navigate to the top of the page)
You have too many side effects in your useEffect hook. Each time one dependency (pageSection or prevPageSection or locale or pageContext.slug) is being changed, it triggers the hook, creating your unwanted effect:
const el = document.getElementById(pageSection[1]);
if (scrollContainer.current && el) {
scrollContainer.current.scrollTo({ top: el.offsetTop });
}
In addition, don't define a function inside the useEffect because it will cause a new scoped function creation each time the effect is triggered, overloading your code and application.
Moreover, don't point directly the DOM using document.getElementById(). It's a high-cost action for the browser and you can achieve the same effect using the useRef hook, you will have the exact same information using the .current property of the useRef hook, in the same way than you do the scrollContainer.current. Indeed, you are creating and manipulating a virtual DOM with React, to avoid pointing the DOM, while your code attacks the real DOM.
Be aware of the use of global objects such as window or document in Gatsby, they are not available during the SSR (Server-Side Rendering) so they may potentially break your code. To summarize, gatsby build compiles the code in your Node server, where there is no window or document because it's not even created yet. The code may work under gatsby develop because it's directly rendered by the browser.
To fix your issue, I would clean the useEffect, you can create as many effects as needed, and you can name them using this notation to make your code more readable:
useEffect(someNamedFunction(){
// your code
}, []).
To bypass it faster, you can create an auxiliary function that forces the page to scroll to certain element without involving the hooks:
const scrollToElement=(el)=>{
scrollContainer.current.scrollTo({ top: el.offsetTop });
}
<button onClick={(el)=> scrollToElement(el)}>

Angular2 - Expression has changed after it was checked - Binding to div width with resize events

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

Issue on steriotab system

I have playing around with a steriotab system with the prototype.js library, everything works fine except the next DIV under the container of steriotabs is showing like a flash when turning to next tab.. I know its little bit difficult to understand.. here you can see it on their website http://stereointeractive.com/blog/code/prototype-tabs/
You can see that by changing the four tabs(features, setup, configuration, download) continuously three four times. The comment section will show up like a flash just below the navigation tabs(Features, Setup, Configuration, Download).
I think the issue was when it goes to next tab the current one is display:none and ofcourse there is nothing in the meantime(1 or 2 seconds) so the next block of html code is coming to the top just below the navigation..
this javascript may causing the issue..
activate: function(tab) {
var tabName = tab.id.replace(this.options.ids.tab,'');
this.currentPanel = this.options.ids.panel+tabName;
if (this.showPanel == this.currentPanel) {
return false;
}
if (this.showPanel) {
if (this.options.effects) {
new Effect.Fade(this.showPanel, {queue: 'front'});
} else {
$(this.currentPanel).hide();
}
}
if (this.options.effects) {
new Effect.Appear(this.currentPanel, {queue: 'end'});
} else {
$(this.showPanel).show();
}
this.tabs.invoke('removeClassName', this.options.classNames.tabActive);
tab.addClassName(this.options.classNames.tabActive);
this.showPanel = this.currentPanel;
}
you guys have any thought?
You're suspicion is correct, the reason you get the flash is the few milliseconds there is no container there holding back the content below the object. One option you could consider is while fading the container also sliding it up (look into parallel effects in scripty) that way it would not be nearly as jarring with the content disappears.

ExtJS: starting HtmlEditor defaulting to source

I'm using ExtJS 3.2.1 and I need a component almost identical to the bundled HtmlEditor, with one exception: it must start editing the HTML source code directly. The reason I don't use a normal TextArea is that the user should be able to preview the result of his actions before submitting.
I've tried calling toggleSourceEdit(), as per ExtJS documentation, with no success. Debugging, I see that the editor object has the sourceEditMode property set to true, and the Source Edit button seems as if it was "pressed", but clicking on it does not render the typed HTML, and clicking it again goes to the Source Mode.
I've tried calling toggleSourceEdit() after the container show() method, on the container afterLayout listener and on the editor afterRender listener. I've tried also calling it on another button that I added to the container. The result is the same on every try.
The only other option I see is updating ExtJS to 3.3.0, but I haven't seem anything related on the changelogs. Either way, it's going to be my next step. EDIT: The app had another problems when updating, we'll make a bigger effort to update later. As of right now, we are using the HtmlEditor in its original setting.
Thanks!
ran into the same problem (using 3.3.0 by the way)
stumbled upon a fix by dumb luck. i have no idea why this works, but second time is the charm. call it twice in a row to achieve the desired effect..
HTMLEditor.toggleSourceEdit(true);
HTMLEditor.toggleSourceEdit(true);
hope that helps!
Rather calling toggleSourceEdit(), try to setup the configuration while you create HtmlEditor Object
Using toggleSourceEdit() caused some problems for me. One was that this seemed to put the editor somewhere in limbo between source edit and WYSIWYG mode unless I used a timeout of 250ms or so. It also puts the focus in that editor, and I don't want to start the form's focus in the editor, especially since it's below the fold and the browser scrolls to the focused html editor when it opens.
The only thing that worked for me was to extend Ext.form.HtmlEditor and then overwrite toggleSourceEdit, removing the focus command. Then adding a listener for toggling to the source editor when the component is initialized. This is for Ext 4.1 and up. For older versions, replace me.updateLayout() with me.doComponentLayout().
var Namespace = {
SourceEditor: Ext.define('Namespace.SourceEditor', {
extend: 'Ext.form.HtmlEditor',
alias: 'widget.sourceeditor',
initComponent: function() {
this.callParent(arguments);
},
toggleSourceEdit: function (sourceEditMode) {
var me = this,
iframe = me.iframeEl,
textarea = me.textareaEl,
hiddenCls = Ext.baseCSSPrefix + 'hidden',
btn = me.getToolbar().getComponent('sourceedit');
if (!Ext.isBoolean(sourceEditMode)) {
sourceEditMode = !me.sourceEditMode;
}
me.sourceEditMode = sourceEditMode;
if (btn.pressed !== sourceEditMode) {
btn.toggle(sourceEditMode);
}
if (sourceEditMode) {
me.disableItems(true);
me.syncValue();
iframe.addCls(hiddenCls);
textarea.removeCls(hiddenCls);
textarea.dom.removeAttribute('tabindex');
//textarea.focus();
me.inputEl = textarea;
} else {
if (me.initialized) {
me.disableItems(me.readOnly);
}
me.pushValue();
iframe.removeCls(hiddenCls);
textarea.addCls(hiddenCls);
textarea.dom.setAttribute('tabindex', -1);
me.deferFocus();
me.inputEl = iframe;
}
me.fireEvent('editmodechange', me, sourceEditMode);
me.updateLayout();
}
})
}
Then to use it:
Ext.create('Namespace.SourceEditor', {
/*regular options*/
listeners: {
initialize: function(thisEditor) {
thisEditor.toggleSourceEdit();
}
}
});
htmlEditor.toggleSourceEdit(true);
one time should be enough if you do this listening to the afterrender event of the editor.

Categories

Resources