How to update the html page view after a timeout in Angular - html

I am trying to display a routerlink name based on a condition. I want to display the div section routerLink name if condition is true.If i check {{isNameAvailable}}, first it displays false and after this.names got the values it shows true.Since in the component getDetails() method is asynchronous this.names getting the values after html template render.Therefore this routerLink does n't display.Therefore I want to display div section after some time. (That 's the solution i have) Don't know whether is there any other solution.
This is my html file code.
<main class="l-page-layout ps-l-page-layput custom-scroll bg-white">
{{isNameAvailable}}
<div class="ps-page-title-head" >
<a *ngIf ="isNameAvailable === true" [routerLink]="['/overview']">{{Name}}
</a>
{{Name}}
</div>
</main>
This is my component.ts file
names= [];
isNameAvailable = false;
ngOnInit() {
this.getDetails()
}
getDetails() {
this.route.params.subscribe(params => {
this.names.push(params.Names);
console.log(this.names);
this.getValues().then(() => {
this.isNameAvailable = this.checkNamesAvailability(this.names);
console.log(this.isNameAvailable);
});
});
}
resolveAfterSeconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 900);
});
}
checkNamesAvailability(names) {
console.log(names);
return names.includes('Sandy');
}
async getValues() {
await this.resolveAfterSeconds(900);
}
And console.log(this.isLevelAvailable); also true. What I can do for this?

1.You do not have anything to show in the HTML only the isNameAvailable, because you do not have any assignment in the Name variable.
2.It is better to use the angular build-in async pipe,
when you want to show the returned value from observables.
3.When you are using the *ngIf directive you can skip *ngIf ="isNameAvailable === true" check because the variable is boolean type, you gust write *ngIf ="isNameAvailable", it will check also for null but NOT for undefined
It is working because the *ngIf directive is responsible for checking and rendering the UI, you can see how many times the directive is checking by calling an function and print and answer in the console.

By any chance do you have changeDetection: ChangeDetectionStrategy.OnPush docs set in component annotation? That might explain this behaviour. With it Angular run change detection only on component #Input()'s changes and since in your case there were non it did not run change detection which is why template was not updated. You could comment that line to check if that was cause of the issue. You are always able to run change detection manually via ChangeDetectorRef.detectChange() docs which should solve you problem
constructor(private cd: ChangeDetectorRef) {}
...
getDetails() {
this.route.params.subscribe(params => {
...
this.getValues().then(() => {
this.isNameAvailable = this.checkNamesAvailability(this.names);
this.cd.detectChanges(); // solution
console.log(this.isNameAvailable);
});
});
}
This stackblitz show this bug and solution. You can read more about change detection here

You could use RxJS timer function with switchMap operator instead of a Promise to trigger something after a specific time.
Try the following
import { Subject, timer } from 'rxjs';
import { takeUntil, switchMap } from 'rxjs/operators';
names= [];
isNameAvailable = false;
closed$ = new Subject();
ngOnInit() {
this.getDetails()
}
getDetails() {
this.route.params.pipe(
switchMap((params: any) => {
this.names.push(params.Names);
return timer(900); // <-- emit once after 900ms and complete
}),
takeUntil(this.closed$) // <-- close subscription when `closed$` emits
).subscribe({
next: _ => {
this.isNameAvailable = this.checkNamesAvailability(this.names);
console.log(this.isNameAvailable);
}
});
}
checkNamesAvailability(names) {
console.log(names);
return names.includes('Sandy');
}
ngOnDestroy() {
this.closed$.next(); // <-- close open subscriptions when component is closed
}

Related

TypeScript - Navigate dropdown listitems using keyboard

I'm working on an open-source project and have encountered a bug. I'm not able to navigate the dropdown list items using the keyboard (arrow key/tab). I've written the keyboard-navigation logic, but not quite sure of how to implement it. Below is the code snippet.
.
.
.
const TopNavPopoverItem: FC<ComponentProps> = ({closePopover, description, iconSize, iconType, title, to}) => {
const history = useHistory();
const handleButtonClick = (): void => {
history.push(to);
closePopover();
};
const useKeyPress = function (targetKey: any) { // where/how am I supposed to use this function?
const [keyPressed, setKeyPressed] = useState(false);
function downHandler(key: any) {
if (key === targetKey) {
setKeyPressed(true);
}
}
const upHandler = (key: any) => {
if (key === targetKey) {
setKeyPressed(false);
}
};
React.useEffect(() => {
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
});
return keyPressed;
};
return (
<button className="TopNavPopoverItem" onClick={handleButtonClick}>
<Icon className="TopNavPopoverItem__icon" icon={iconType} size={iconSize} />
<div className="TopNavPopoverItem__right">
<span className="TopNavPopoverItem__title">{title}</span>
<span className="TopNavPopoverItem__description">{description}</span>
</div>
</button>
);
};
Any workaround or fixes?
Thanks in advance.
A custom hook should always be defined at the top level of your file. It cannot be inside of a component. The component uses the hook, but doesn't own the hook.
You have a hook which takes a key name as an argument and returns a boolean indicating whether or not that key is currently being pressed. It's the right idea, but it has some mistakes.
When you start adding better TypeScript types you'll see that the argument of your event listeners needs to be the event -- not the key. You can access the key as a property of the event.
(Note: Since we are attaching directly to the window, the event is a DOM KeyboardEvent rather than a React.KeyboardEvent synthetic event.)
Your useEffect hook should have some dependencies so that it doesn't run on every render. It depends on the targetKey. I'm writing my code in CodeSandbox where I get warnings about "exhaustive dependencies", so I'm also adding setKeyPressed as a dependency and moving the two handlers inside the useEffect.
I see that you have one handler as function and one as a const. FYI it really doesn't matter which you use in this case.
Our revised hook looks like this:
import { useState, useEffect } from "react";
export const useKeyPress = (targetKey: string) => {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(
() => {
const downHandler = (event: KeyboardEvent) => {
if (event.key === targetKey) {
setKeyPressed(true);
}
};
const upHandler = (event: KeyboardEvent) => {
if (event.key === targetKey) {
setKeyPressed(false);
}
};
// attach the listeners to the window.
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
// remove the listeners when the component is unmounted.
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
};
},
// re-run the effect if the targetKey changes.
[targetKey, setKeyPressed]
);
return keyPressed;
};
I don't know you intend to use this hook, but here's a dummy example. We show a red box on the screen while the spacebar is pressed, and show a message otherwise.
Make sure that the key name that you use when you call the hook is the correct key name. For the spacebar it is " ".
import { useKeyPress } from "./useKeyPress";
export default function App() {
const isPressedSpace = useKeyPress(" ");
return (
<div>
{isPressedSpace ? (
<div style={{ background: "red", width: 200, height: 200 }} />
) : (
<div>Press the Spacebar to show the box.</div>
)}
</div>
);
}
CodeSandbox Link

Boolean value doesn't change

I have a problem. After searching for hours I cannot find an explanation for this. I want to display a modal (from primeNG) and show it when the user clicks a button. This button calls (with an id) to my API REST and brings information, very simple. I receive the information, but when the modal should show, this doesn't happen.
map.component.ts
export class MapComponent implements OnInit {
public alteraciones: any[];
public alteracion: any = {};
display: boolean = false;
/*...*/
generateData(map: L.map) {
const data: any[] = [];
let marker: any;
L.geoJson(this.alteraciones, {
pointToLayer: (feature, latlng) => {
marker = L.marker(latlng, {
icon: this.getIconMarker(feature.properties.tipo_alteracion)
});
marker.on('click', (e) => {
this.getInfoAlteracion(feature.properties.id_alteracion); // <==
});
data.push(marker);
}
});
/*...*/
}
/**...**/
getInfoAlteracion(id_alteracion: string) {
this.mapService.getInfoAlteracion(id_alteracion).subscribe(
result => {
this.alteracion = result;
console.log(this.alteracion); // < == Information OK
this.display = true; // <== this variable should change but doesn't
},
err => console.log(err)
);
}
}
map.component.html
<p-dialog header="Info" [(visible)]="display" modal="modal" width="500" [responsive]="true">
<!--some code-->
<p-footer>
<button type="button" pButton icon="fa-close" (click)="display=false" label="Cerrar"></button>
</p-footer>
</p-dialog>
However, when I recompile or when I turn off the server, the value of the display variable changes, and it shows me the modal. I cannot find an explanation, any idea?
EDIT
Posible conflicts:
#asymmetrik/ngx-leaflet: 3.0.2
#asymmetrik/ngx-leaflet-markercluster: 1.0.0
EDIT 2
I also added a new marker with a new variable to change but doesn't work too. At this point, I think (and I'm 90% sure) that it's a problem of communication between component.ts and component.html.
Try to make that boolean display property public !
Finally, I solved the problem.Thanks to this link, I realized that it was a problem of compatibility between libraries. Leaflet event handlers run outside of Angular's zone, where changes to input bound fields will not be detected automatically. To ensure my changes are detected and applied, I need to make those changed inside of Angular's zone. Adding this to the code, finally, all works:
constructor(private mapService: MapService, private zone: NgZone) { }
marker.on('click', (e) => {
this.zone.run(() => {
this.getInfoAlteracion(feature.properties.id_alteracion);
});
});
data.push(marker);
}
Thanks to all for the help!

Angular2 : TypeError 'this.' is undefined

I wrote a function that gets and shows the "best player" from my array of objects (the player who has most likes). The function works fine and shows me what I want, but the browser shows me errors in the console, and the routing in my app is blocked (I can't navigate between components using routes)
this is my DashboardComponent Class
export class DashboardComponent implements OnInit {
bestPlayer:Player;
data:Player[]=[];
max:number =0;
constructor(private playerService : PlayerService ,private router: Router) { }
ngOnInit() {
this.playerService.getData().then(data => {
this.data = data;
for (var i = 0; i <= this.data.length; i++) {
if (this.data[i].likes>this.max) {
this.max=this.data[i].likes;
this.bestPlayer=this.data[i];
}
}
});
}
viewDetails(bestPlayer: Player):void {
this.router.navigate(['/detail',this.bestPlayer.id]);
}
}
This is my service:
import {Player} from './player';
import {PlayersData} from './muck-players';
import { Injectable } from '#angular/core';
#Injectable()
export class PlayerService {
players:any;
data:any;
Player:Player;
getData():Promise <Player[]> {
return Promise.resolve(PlayersData);
}
}
when I run the app the browser show me those errors :
TypeError: this.data[i] is undefined
Error: Uncaught (in promise): Error: Cannot activate an already activated outlet
Error: Uncaught (in promise): Error: Cannot activate an already activated outlet
when I delete whats in ngOninit() and viewDetails() function, routing starts working again and browser doesn't show me errors.
Any help please !
As a sidenote, always when you provide a plunker, make sure it's a working one ;) When I got it working, there was only a couple of issue actually. There was nothing wrong with your routing. The first error you were getting
this.data[i] is undefined
is because of your for loop, you had marked that we should loop until i matches the length of the array (or equal). But we need to remember that the index of arrays start with 0. So the last iteration it was trying to read an index that was not present in your array. So you should add -1 to your for loop:
for (var i = 0; i <= this.data.length-1; i++)
or do
for (var i = 0; i < this.data.length; i++)
When this was fixed, it produces new issues. Since data is coming async, so by the time the template is rendered, your variable bestPlayer is undefined. This can be fixed with safe navigation operator, or wrapping everything inside a div with the condition that bestPlayer has values. This need to be applied to both the detail page and the dashboard. With the dashboard:
<div *ngIf="bestPlayer">
<!-- code here -->
</div>
And in the detail page the same but using player instead, as that is the variable you are using there.
As mentioned, you can also use the safe navigation operator.
These actually cleared the second error you also had.
Here's your fixed PLUNKER.
I have fixed dozens of errors in the Plunker and added some missing routing features. The App is working just fine now please take a look at my forked Plunker over here
I fixed all the files paths and used in forEach method in your component just like this:
ngOnInit() {
this.playerService.getData().then(data => {
data.forEach( (arrData) => {
if (arrData.likes>this.max) {
this.max=arrData.likes;
this.bestPlayer=arrData;
}
})
});
}
Demonstration:
Hi the issue is with your service,
You are trying to loop through data that is not available,
You need to change the code ,
If you are using Observer as service then put you code inside .subscribe method,
If you are using promise then put your looping code inside .then() method.
Try to use this :
If you are returning promise from this.playerService.getData() this service
this.playerService.getData().then((data) => {
this.data = data;
for (var i = 0; i <= this.data.length; i++) {
if (this.data[i].likes>this.max) {
this.max=this.data[i].likes;
this.bestPlayer=this.data[i];
}
})
If you are returning observable from this.playerService.getData() this service
this.playerService.getData().subscribe((data) => {
this.data = data;
for (var i = 0; i <= this.data.length; i++) {
if (this.data[i].likes>this.max) {
this.max=this.data[i].likes;
this.bestPlayer=this.data[i];
}
})

JS import - VueJs Router - having trouble refactoring a watch object

This question is specific to vuejs router, however may simply be a misunderstanding of importing js objects and assigning to the window object.
I am watching for url changes on a page which works fine with the watcher code in the component file. I need to use the same watcher code for multiple components so I extracted it to its own file, assigned it to the global scope, and cannot get it to work. Here are the details:
Working code in with the watcher in the component:
watch:{
$route () {
console.log('route changed')
//was it a reset?
console.log( this.$route.query.sort)
if(this.$route.query.sort === undefined){
if(this.$route.meta.reset){
//reset was pressed... actually do nothing here
this.$route.meta['reset'] = false;
}
else{
this.loading = true;
this.searchableTable.removeResultsTable();
this.searchableTable.options.search_query = this.$route.fullPath;
this.searchableTable.updateSearchPage();
}
}
else
{
//sort change just update the table view
}
}
}
So then I extracted the watch to a file routeWatcher.js:
export default {
$route () {
console.log('route changed')
//was it a reset?
console.log(this.$route.query.sort)
if (this.$route.query.sort === undefined) {
if (this.$route.meta.reset) {
//reset was pressed... actually do nothing here
this.$route.meta['reset'] = false;
}
else {
this.loading = true;
this.searchableTable.removeResultsTable();
this.searchableTable.options.search_query = this.$route.fullPath;
this.searchableTable.updateSearchPage();
}
}
else {
//sort change just update the table view
}
}
}
then I import and use, which works fine....
import searchableTableRouteWatcher from '../../controllers/routeWatcher'
...
watch:searchableTableRouteWatcher
again works fine.
Now the problem - I want to avoid the import in multiple files, so I thought I could put it on the window as a global
in my main.js file:
import searchableTableRouteWatcher from './controllers/routeWatcher'
window.searchableTableRouteWatcher = searchableTableRouteWatcher;
Then in my component:
watch:searchableTableRouteWatcher
results in searchableTableRouteWatcher is not defined
watch:window.searchableTableRouteWatcher
results in no errors, but the code is not being called
I have a feeling it has to do with this and there is confusion on $route()
For your purpose there are 'Mixins' in Vue.js: documentation
What you can do:
create a file, say mixins/index.js:
export const routeWatcher = {
watch: {... your watcher code pasted here ... }
};
import into your component:
import { routeWatcher } from 'path/to/mixins/index';
add mixin to your component properties and methods:
<script>
export default {
mixins: [routeWatcher];
data () ...... all your original component's script content
}
Mixin's content will be merged with component's original properties and act if it was hardcoded there.
Addition after your comment:
You can also declare Mixin globally, like this:
above 'new Vue' declaration put this code:
Vue.mixin({
watch: {....}
});
This mixin will appear in every component.

How to pass a thunk or callback function into a redux action. Serializing functions in a redux store for modals and toast confirm notifications

When using a generic modal or toast with a confirm button, it becomes useful to be able to pass an action into this component so it can be dispatched when you click confirm.
The action may look something like this:
export function showConfirm({modalConfirm}) {
return {
type: 'MODALS/SHOW_MODAL',
payload: {
modalId: getUuid(),
modalType: 'CONFIRM',
modalConfirm : modalConfirm,
},
};
}
Where modalConfirm is another action object such as:
const modalConfirm = {
type: 'MAKE_SOME_CHANGES_AFTER_CONFIRM',
payload: {}
}
The modalConfirm action is dispatched inside the modal component using dispatch(modalConfirm) or even dispatch(Object.assign({}, modalConfirm, someResultFromTheModal)
Unfortunatley this solution only works if modalConfirm is a simple redux action object. This system is clearly very limited. Is there anyway you can pass a function (such as a thunk) in instead of a simple object?
Ideally, something full featured likes this:
const modalConfirm = (someResultFromTheModal) => {
return (dispatch, getState){
dispatch({
type: 'MAKE_SOME_UPDATES',
payload: someResultFromTheModal
})
dispatch({
type: 'SAVE_SOME_STUFF',
payload: http({
method: 'POST',
url: 'api/v1/save',
data: getState().stuffToSave
})
})
}
}
Funny, putting an action object in the store and passing it as a prop to a generic dialog is exactly the approach I came up with myself. I've actually got a blog post waiting to be published describing that idea.
The answer to your question is "Yes, but....". Per the Redux FAQ at http://redux.js.org/docs/FAQ.html#organizing-state-non-serializable , it's entirely possible to put non-serializable values such as functions into your actions and the store. However, that generally causes time-travel debugging to not work as expected. If that's not a concern for you, then go right ahead.
Another option would be to break your modal confirmation into two parts. Have the initial modal confirmation still be a plain action object, but use a middleware to watch for that being dispatched, and do the additional work from there. This is a good use case for Redux-Saga.
I ended up using string aliases to an actions library that centrally registers the actions.
Modal emmiter action contains an object with functionAlias and functionInputs
export function confirmDeleteProject({projectId}) {
return ModalActions.showConfirm({
message: 'Deleting a project it permanent. You will not be able to undo this.',
modalConfirm: {
functionAlias: 'ProjectActions.deleteProject',
functionInputs: { projectId }
}
})
}
Where 'ProjectActions.deleteProject' is the alias for any type of complicated action such as:
export function deleteProject({projectId}) {
return (dispatch)=>{
dispatch({
type: 'PROJECTS/DELETE_PROJECT',
payload: http({
method: 'DELETE',
url: `http://localhost:3000/api/v1/projects/${projectId}`,
}).then((response)=>{
dispatch(push(`/`))
}),
meta: {
projectId
}
});
}
}
The functions are registered in a library module as follows:
import * as ProjectActions from '../../actions/projects.js';
const library = {
ProjectActions: ProjectActions,
}
export const addModule = (moduleName, functions) => {
library[moduleName] = functions
}
export const getFunction = (path) => {
const [moduleName, functionName] = path.split('.');
// We are getting the module only
if(!functionName){
if(library[moduleName]){
return library[moduleName]
}
else{
console.error(`Module: ${moduleName} could not be found.`);
}
}
// We are getting a function
else{
if(library[moduleName] && library[moduleName][functionName]){
return library[moduleName][functionName]
}
else{
console.error(`Function: ${moduleName}.${functionName} could not be found.`);
}
}
}
The modalConfirm object is passed in to the modal by props. The modal component requires the getFunction function in the module above. The modalConfirm object is transformed into a function as follows:
const modalConfirmFunction = (extendObject, modalConfirm) => {
const functionFromAlias = getFunction(modalConfirm.functionAlias);
if(functionFromAlias){
dispatch(functionFromAlias(Object.assign({}, modalConfirm.functionInputs, extendObject)));
}
}
As you can see, this function can take in inputs from the modal. It can execute any type of complicated action or thunk. This system does not break time-travel but the centralized library is a bit of a drawback.