I'm using an internal private reusable component. My problem is that the width is not being dynamically updated when the viewport is updated. Here are snippets of relevant code:
component.ts
export class Component {
modalWidth: string | undefined;
ngOnInit() {
this.breakpointServiceSubscription$ = this.breakpointService.breakpoint$.subscribe(() => {
if (this.breakpointService.isSmall()) {
console.log("small")
this.modalWidth = "50px";
}
else {
this.modalWidth = "500px";
}
}
}
component.html
<modal [width]="modalWidth">...</modal>
The width and height are supposed to change dynamically as the browser is resized, but it stays the same size as when it was rendered. If I open the modal in a specific viewport the size is always correct, it's only a problem once I am trying to resize with the modal open.
When logging the subscription to the breakpoint service, it is always correct and will log dynamically.
I've tried converting modalWidth and modalHeight to observables and using an async pipe in the html but it still has the same behaviour.
Any tips or suggestions?
you can inject ChangeDetectorRef in the component and after changing modalWidth, call changeDetectorRef.detectChanges() to let angular apply the change immediately to the view.
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.breakpointServiceSubscription$ = this.breakpointService.breakpoint$.subscribe(() => {
if (this.breakpointService.isSmall()) {
console.log("small")
this.modalWidth = "50px";
}
else {
this.modalWidth = "500px";
}
// apply change immediately
this.cdr.detectChanges();
}
In my Angular2 application there are two components for form view and graph view. In the form, there is a spinner and checkbox for auto refresh handle.
Form Component html
<div class="refresh-spinner">
<my-checkbox
[selected]=autoRefresh
[disabled]="false"
[id]="'autoRefresh'"
[name]="'Auto Refresh Every'"
[code]="'autoRefresh'"
(status)="checkAutoRefresh($event)">
</my-checkbox>
<my-spinner [title]="''"
[category]="'duration'"
[maxCount]=maxRefreshTime
[minCount]=minRefreshTime
[value]=minRefreshTime //default refresh value is 1 min
[editable]="true"
(count)="getAutoRefreshInterval($event)">
</my-spinner>
<span class="post__text"> Mins</span>
</div>
Form Component ts
// form view emits selected criteria
#Output() criteria = new EventEmitter<any>();
checkAutoRefresh(ele) {
this.autoRefresh = ele.selected;
localStorage.setItem('autoRefresh', ele.selected);
}
getAutoRefreshInterval(interval) {
localStorage.setItem('refreshInterval', interval.value);
}
Refresh interval and checkbox value (autoRefresh true/fasle) are set is local storage on spinner event and checkbox select event.
Graph components ts
alive: boolean; // used to unsubscribe from the IntervalObservable when OnDestroy is called.
#Input() criteria: FilteringCriteria;
constructor(private element: ElementRef, private myService: MyService) {
this.alive = true;
}
ngOnInit() {
let interval: number = JSON.parse(localStorage.getItem('refreshInterval'));
console.log(Date());
this.getStatistics();
IntervalObservable.create(interval * 60 * 1000)
.takeWhile(() => this.alive)
.subscribe((e) => {
// console.log(e)
if (JSON.parse(localStorage.getItem('autoRefresh'))) {
console.log(Date());
this.getStatistics();
}
});
}
ngOnDestroy() {
this.alive = false;
}
These form view and graph view are used in main component as below.
<search-criteria (criteria)="emitCriteria($event)"></search-criteria> //filteringCriteria emits from here
<ng-template ngFor let-result [ngForOf]="servers" let-i="index">
<my-grid-item row [class]="'graph--view'"
[colspan]="4">
<graph-view [criteria]="filteringCriteria"></graph-view>
</my-grid-item>
</ng-template>
Two Questions:
1. Once I check auto refresh checkbox graphs are refresh in 1 minute. But time interval is calculating from the time component is initialized not from the time the checkbox is selected.
2 If I change the value of the spinner (from 1 min to 2 min) local storage value is changed to new value 2. But graphs are refreshing in 1 min time intervals. But if I hit on form done button, then the graphs are refreshing in new time interval(2 min).
Any suggestions are appreciated.
Thank You!
It is happening because you are initializing the Observable as part of the init process of the graph component. So the time is taken from the moment it is initialized and when you update the interval it does not know about that and keeps using the one with which it was initialized.
You can declare a variable to hold the subscription and move all your code to subscribe to a different method. Something like
subscription;
constructor(private element: ElementRef, private myService: MyService) {
}
ngOnInit() {
this.getThreadPoolStatistics();
this.autoUpdateInit();
// subscribe to autoRefresh and interval changes
// autoRefreshChange.subscribe( () => this.autoUpdateInit());
// intervalChange.subscribe( () => this.autoUpdateInit());
}
autoUpdateInit(){
let interval: number = JSON.parse(localStorage.getItem('refreshInterval'));
console.log(Date());
// remove the old subscription
if(subscription) {
subscription.unsubscribe();
subscription = null;
}
// create one only when you need it. check for autorefresh and alive?
if(<subscription required>){
subscription = IntervalObservable.create(interval * 60 * 1000)
.subscribe((e) => {
// console.log(e)
if (JSON.parse(localStorage.getItem('autoRefresh'))) {
console.log(Date());
this.getStatistics();
}
});
}
}
You have to make sure, your Graph component gets the updates to the autorefresh and interval and call the autoUpdateInit again to make sure it updates the IntervalObservable. You can use a service to make sure both the components are looking at the same values as per this answer. If they are having a parent-child relation, then you can emit the changes via the #Output.
My React native application screen has View component with few text inputs. How can touch be detected on screen outside that View? Please help.
Thanks
As Andrew said: You can wrap your View with TouchableWithoutFeedback and adding a onPress you can detect when the view is tapped.
Another way to achieve that is having responses for touch events from the view.
/* Methods that handled the events */
handlePressIn(event) {
// Do stuff when the view is touched
}
handlePressOut(event) {
// Do stuff when the the touch event is finished
}
...
<View
onStartShouldSetResponder={(evt) => true}
onMoveShouldSetResponder={(evt) => true}
onResponderGrant={this.handlePressIn}
onResponderMove={this.handlePressIn}
onResponderRelease={this.handlePressOut}
>
...
</View>
The difference between Grant and move is that Grant is just when the user press, and Move is when the user is pressing and moving the position of the press
I don't take no for an answer, so I dug up a lot to find a solution matching my needs.
In my situation I have multiple components which need to collapse when I open another one.
This behavior has to be automatic, and easy to code-in by any contributor.
Passing parent refs to the children or calling a special global method are not acceptable solutions in my circumstances.
Using a transparent background to catch all clicks will not cut it.
This Question perfectly illustrates the need.
Demo
Here is the final result. Clicking anywhere except the component itself will collapse it.
WARNING
The solution includes usage of private React components properties. I know the inherent risks of using such an approach and I'm happy to use them as long as my app does what I expect and all other constraints are satisfied. Short disclaimer, probably a smarter, cleaner solution exists out there. This is the best I could do with my own limited knowledge of React.
First we need to capture all click in the UI, both for Web and Native. It seems that this is not easily done. Nested TouchableOpacityseem to allow only one responder at a time. So I had to improvise a bit here.
app.tsx (trimmed down to essentials)
import * as div from './app.style';
import { screenClicked, screenTouched } from './shared/services/self-close-signal.service';
// ... other imports
class App extends React.Component<Props, State> {
public render() {
return (
<div.AppSafeArea
onTouchStart={e => screenTouched(e)}
onClick={e => screenClicked(e)}>
{/* App Routes */}
<>{appRoutes(loginResponse)}</>
</div.AppSafeArea>
);
}
}
self-close-signal.service.ts
This service was built to detect all clicks on the app screen. I use reactive programming in the entire app so rxjs was employed here. Feel free to use simpler methods if you want. The critical part here is detecting if the clicked element is part of the hierarchy of an expanded component or not. When I write a mess like this I usually fully document why this was built this way in order to protect it from "eager" developers doing cleanups.
import { AncestorNodeTrace, DebugOwner, SelfCloseEvent } from '../interfaces/self-close';
import { GestureResponderEvent } from 'react-native';
import { Subject } from 'rxjs';
/**
* <!> Problem:
* Consider the following scenario:
* We have a dropdown opened and we want to open the second one. What should happen?
* The first dropdown should close when detecting click outside.
* Detecting clicks outside is not a trivial task in React Native.
* The react events system does not allow adding event listeners.
* Even worse adding event listener is not available in react native.
* Further more, TouchableOpacity swallows events.
* This means that a child TouchableOpacity inside a parent TouchableOpacity will consume the event.
* Event bubbling will be stopped at the responder.
* This means simply adding a backdrop as TouchableOpacity for the entire app won't work.
* Any other TouchableOpacity nested inside will swallow the event.
*
* <!> Further reading:
* https://levelup.gitconnected.com/how-exactly-does-react-handles-events-71e8b5e359f2
* https://stackoverflow.com/questions/40572499/touchableopacity-swallow-touch-event-and-never-pass
*
* <!> Solution:
* Touch events can be captured in the main view on mobile.
* Clicks can be captured in the main view on web.
* We combine these two data streams in one single pipeline.
* All self closeable components subscribe to this data stream.
* When a click is detected each component checks if it was triggered by it's own children.
* If not, it self closes.
*
* A simpler solution (with significant drawbacks) would be:
* https://www.jaygould.co.uk/2019-05-09-detecting-tap-outside-element-react-native/
*/
/** Combines both screen touches on mobile and clicks on web. */
export const selfCloseEvents$ = new Subject<SelfCloseEvent>();
export const screenTouched = (e: GestureResponderEvent) => {
selfCloseEvents$.next(e);
};
export const screenClicked = (e: React.MouseEvent) => {
selfCloseEvents$.next(e);
};
/**
* If the current host component ancestors set contains the clicked element,
* the click is inside of the currently verified component.
*/
export const detectClickIsOutside = (event: SelfCloseEvent, host: React.Component): boolean => {
let hostTrace = getNodeSummary((host as any)._reactInternalFiber);
let ancestorsTrace = traceNodeAncestors(event);
let ancestorsTraceIds = ancestorsTrace.map(trace => trace.id);
let clickIsOutside: boolean = !ancestorsTraceIds.includes(hostTrace.id);
return clickIsOutside;
};
// ====== PRIVATE ======
/**
* Tracing the ancestors of a component is VITAL to understand
* if the click originates from within the component.
*/
const traceNodeAncestors = (event: SelfCloseEvent): AncestorNodeTrace[] => {
let ancestorNodes: AncestorNodeTrace[] = [];
let targetNode: DebugOwner = (event as any)._targetInst; // <!WARNING> Private props
// Failsafe
if (!targetNode) { return; }
traceAncestor(targetNode);
function traceAncestor(node: DebugOwner) {
node && ancestorNodes.push(getNodeSummary(node));
let parent = node._debugOwner;
parent && traceAncestor(parent);
}
return ancestorNodes;
};
const getNodeSummary = (node: DebugOwner): AncestorNodeTrace => {
let trace: AncestorNodeTrace = {
id: node._debugID,
type: node.type && node.type.name,
file: node._debugSource && node._debugSource.fileName,
};
return trace;
};
interfaces/self-close.ts - Some boring typescript interfaces to help with project maintenance.
import { NativeSyntheticEvent } from 'react-native';
/** Self Close events are all the taps or clicks anywhere in the UI. */
export type SelfCloseEvent = React.SyntheticEvent | NativeSyntheticEvent<any>;
/**
* Interface representing some of the internal information used by React.
* All these fields are private, and they should never be touched or read.
* Unfortunately, there is no public way to trace parents of a component.
* Most developers will advise against this pattern and for good reason.
* Our current exception is an extremely rare exception.
*
* <!> WARNING
* This is internal information used by React.
* It might be possible that React changes implementation without warning.
*/
export interface DebugOwner {
/** Debug ids are used to uniquely identify React components in the components tree */
_debugID: number;
type: {
/** Component class name */
name: string;
};
_debugSource: {
/** Source code file from where the class originates */
fileName: string;
};
_debugOwner: DebugOwner;
}
/**
* Debug information used to trace the ancestors of a component.
* This information is VITAL to detect click outside of component.
* Without this script it would be impossible to self close menus.
* Alternative "clean" solutions require polluting ALL components with additional custom triggers.
* Luckily the same information is available in both React Web and React Native.
*/
export interface AncestorNodeTrace {
id: number;
type: string;
file: string;
}
And now the interesting part.
dots-menu.tsx - Trimmed down to the essentials for the example
import * as div from './dots-menu.style';
import { detectClickIsOutside, selfCloseEvents$ } from '../../services/self-close-signal.service';
import { Subject } from 'rxjs';
// ... other imports
export class DotsMenu extends React.Component<Props, State> {
private destroyed$ = new Subject<void>();
constructor(props: Props) {
// ...
}
public render() {
const { isExpanded } = this.state;
return (
<div.DotsMenu ...['more props here'] >
{/* Trigger */}
<DotsMenuItem expandMenu={() => this.toggleMenu()} ...['more props here'] />
{/* Items */}
{
isExpanded &&
// ... expanded option here
}
</div.DotsMenu>
);
}
public componentDidMount() {
this.subscribeToSelfClose();
}
public componentWillUnmount() {
this.destroyed$.next();
}
private subscribeToSelfClose() {
selfCloseEvents$.pipe(
takeUntil(this.destroyed$),
filter(() => this.state.isExpanded)
)
.subscribe(event => {
let clickOutside = detectClickIsOutside(event, this);
if (clickOutside) {
this.toggleMenu();
}
});
}
private toggleMenu() {
// Toggle visibility and animation logic goes here
}
}
Hope it works for you as well.
P.S. I'm the owner, feel free to use these code samples. Hope you will enjoy this answer and check Visual School for future React Native tutorials.
Put your View inside of TouchableWithoutFeedback, expand TouchableWithoutFeedback fullscreen and add onPress handler to it.
<TouchableWithoutFeedback
onPress={ /*handle tap outside of view*/ }
style={ /* fullscreen styles */}
>
<View>
...
</View
</TouchableWithoutFeedback>
You could try to use a Modal to create this behavior.
When you click the input field you show the Modal containing the multiple texts inputs. If you click outside the Modal it hides.
you can use
<View>
<TouchableWithoutFeedback
onPress={()=>{
//do something
}}
style={{position:'absolute',top:0 , right:0 , bottom:0 ,left:0}}/>
<YourComp></YourComp>
</View>
An easier solution, as stated here, is to detect the start of a touch action outside of the menu and close the menu in this case.
Keep in mind that for this to work, the first View that will catch the touch should take the full screen height, and that the app content as well as the menu should be inside. This allow the touch event to cascade correctly.
eg:
const [isOverflowMenuDisplayed, setOverflowMenuDisplayed] = useState(false)
const [childrenIds, setChildrenIds] = useState([])
const handleTouchShouldSetResponder = (event) => {
// To be able to close the overflow menu, the content of the screen need to be inside this top view, and detect if the pressed view if the menu item or the app content
if (childrenIds.length) {
if (childrenIds.includes(event.target)) {
return true
}
setOverflowMenuDisplayed(false)
return false
}
return false
}
return <View
onStartShouldSetResponder={handleTouchShouldSetResponder}
onMoveShouldSetResponder={handleTouchShouldSetResponder}>
<AppBar title={title} onLeftIconPress={onLeftIconPress} isCloseLeftIcon={isCloseLeftIcon}>
{actions}
{overflowAction && <AppBarActionOverflow onOpen={() => setOverflowMenuDisplayed(true)} />}
</AppBar>
<AppBarOverflowMenu
overflowAction={overflowAction}
isOpen={isOverflowMenuDisplayed}
childrenIds={childrenIds}
setChildrenIds={setChildrenIds}
onPress={() => setOverflowMenuDisplayed(false)}
/>
{children}
</View>
And the Overflow menu:
export const AppBarOverflowMenu = ({ isOpen, setChildrenIds, childrenIds, onPress, overflowAction }) => {
if (!isOpen) {
return null
}
return (
<View
style={thisStyles.menuContainer}
ref={(component) => {
if (component) {
const ids = component._children[0]._children.map((el) => el._nativeTag)
if (ids.length > 0 && (childrenIds.length !== ids.length || !childrenIds.includes(ids[0]))) {
setChildrenIds(ids)
}
}
}}>
<View style={thisStyles.menu}>
{React.cloneElement(overflowAction, {
onPress: () => {
onPress(false)
overflowAction.props.onPress()
},
})}
</View>
</View>
)
}
I'm making an angular directive that hides element a when the user scrolls on element b. It works fine, but I can't figure out this behaviour:
It might be hard to tell, but essentially when you scroll to the bottom the scroll bar expands because element a sits above element b, so essentially the thing I'm scrolling on has more space available. After that, I'm not sure why it scrolls up. Here's a gif of the full page if that makes it any clearer:
My directive is written in typescript (angular version 1.5.7 NOT 2.x), I'll work on translating it to javascript but in the interest of getting this question out there as quickly as possible here's the ts:
interface IScope extends ng.IScope {
showHeader: boolean;
}
export class IncodeHideHeaderDirective implements ng.IDirective {
restrict = "AE";
require: "ngModel";
scope: Object;
replace = true;
link: ng.IDirectiveLinkFn | ng.IDirectivePrePost;
oldy: number;
justScrolled = false;
constructor() {
const self = this;
this.link = (scope: IScope, element: ng.IAugmentedJQuery) =>
{
element.bind("scroll",
() => {
if (element[0].scrollTop > self.oldy) {
console.log("collapsing");
scope.$eval("showHeader=false");
}
else if (element[0].scrollTop < self.oldy)
{
console.log("expanding");
scope.$eval("showHeader=true");
}
self.oldy = element[0].scrollTop;
}
);
element.bind("load",
() => {
console.log(scope);
this.oldy = element[0].scrollTop;
});
};
}
public static factory(): ng.IDirectiveFactory {
const directive = () => new IncodeHideHeaderDirective();
return directive;
}
}
angular.module("incode.module")
.directive("ixHeader", incode.directives.label.IncodeHideHeaderDirective.factory());
pretty basic stuff. How do I get the scrollbar to stop doing this weird stuff?
Here's a fiddle demonstrating the problem.
That's not an Angular answer, but why not just remove your header bar from the flow by making it position: fixed (or absolute) ? It would not cause the reflow of the main contents, and you would have none of the issues you are experiencing.
.slideUp {
/* ... */
position: fixed;
}
https://jsfiddle.net/e8j938g8/3/
In Angular 2 I am trying to animated in new components via the Router onActivate method.
I have set up a Plunk with a demonstration of the issue here:
http://plnkr.co/FikHIEPONMYhr6COD9Ou
An example of the onActivate method in one of the page components:
routerOnActivate(next: ComponentInstruction, prev: ComponentInstruction) {
document.getElementsByTagName("page3")[0].className='animateinfromright';
}
The issue that I'm having is that I want the new components to animate in on top of the existing component, but the old component is removed from the DOM before the new component is added.
Is there any way to delay the removal of the previous page while the new one animates in?
I found this similar issue: Page transition animations with Angular 2.0 router and component interface promises
but the technique just delays the removal of the previous component before the new one is added.
Eventually I will have different animations depending on which page we are moving from / to, hence having the onActivate in each of the page components.
Many thanks for any help!
You could add an "EchoComponent" where your <router-outlet> is, create a <canvas> in it and drawImage() on routerOnDeactivate()... Something like:
#Component({
template: `<canvas #canvas *ngIf="visible"></canvas>`
})
class EchoComponent {
#ViewChild("canvas") canvas;
public visible = false;
constructor(private _shared: SharedEmitterService) {
this._shared.subscribe(el => el ? this.show(el) : this.hide(el));
}
show(el) {
this.canvas.drawImage(el);
this.visible = true;
}
hide() {
this.visible = false;
}
}
#Component({...})
class PrevRoute {
constructor(private _eref: ElementRef,
private _shared: SharedEmitterService) {}
routerOnDeactivate {
this._shared.emit(this._eref.nativeElement);
}
}
#Component({...})
class NextRoute {
constructor(private _eref: ElementRef,
private _shared: SharedEmitterService) {}
routerOnActivate {
this._shared.emit(false);
}
}
This is just a pseudo code (writing it from memory), but it should illustrate what would you need for this approach.