Async Loading of Hierachical Kendo Treeview using Angular - async-await

I'm trying to load a hierarchical treeview from an external source, but I'm having difficulties loading the first node childs and most likey the inner childs aswell (but this I don't know yet..).
The data is loaded through axios, so the RxJS Observable methods won't work. I tried may different things but when I'm expanding the first node the spinner just keeps on spinning until eternity.
Some of the points that I have tried
Tried to assign the parent node with the childeren
Tried an async modifier
Tried many different things with hasChilderen function
Hopefully someone can point me in the right direction.
My code:
tree.component.html
<kendo-treeview [nodes]="locations | async" [textField]="['shortName']" kendoTreeViewExpandable
[hasChildren]="hasChildren.bind(this)" [children]="fetchChildren.bind(this)">
</kendo-treeview>
tree.component.ts
export class TreeComponent implements OnInit {
public locations: Promise<Location[]>
constructor(private locationService: LocationService,
private cdRef: ChangeDetectorRef) { }
ngOnInit() {
this.locations = this.locationService.getBaseLocation();
}
public hasChildren = (item: Location): boolean => item.hasChildLocations;
public fetchChildren = (item: Location): Promise<Location[]> => this.locationService.getChildLocations(item.locationId);
}
location.service.ts
export class LocationService {
async getBaseLocation(): Promise<Location[]> {
return await axios.get<Location>('/Location/GetBaseLocation')
.then((response: AxiosResponse<Location>) => {
return [response.data];
});
}
async getChildLocations(locationId: string): Promise<Location[]> {
return await axios.get<Location[]>(`/Location/GetLocationTree?UpperLocationID=${locationId}`)
.then((response: AxiosResponse<Location[]>) => response.data);
}
}
location.model.ts
export interface Location {
locationId: string;
upperLocationId: string;
locationTypeId: number;
shortName: string;
plantCode: string;
hasChildLocations: boolean;
icon: string;
isSelected: boolean;
childeren: Location[];
}
e.g. Spinner keeps rolling
console prints out an error before my childs are loaded from the external service, so my 5 cents are on the fact that angular is trying to expand the nodes before they are loaded.

Easy solution:
Change
public fetchChildren = (item: any) this.locationService.getChildLocations(item.locationId);
to
public fetchChildren = (item: any) => from(this.locationService.getChildLocations(item.locationId));

Related

How to resolve two fields in one #ResolveField method

I have orders resolver, like this one(thats an example, not actually problem):
#Resolver(() => OrderEntity)
export class OrderResolver {
constructor(
private readonly orderService: OrderService,
private readonly usersService: UsersService,
) {}
.................
#ResolveField('users', () => [UsersEntity])
async users(#Parent() order: OrderEntity): Promise<UserEntity[]> {
return await this.usersService.findAllByOrderId(order.id);
}
#ResolveField('usersCount', () => Int)
async usersCount(#Parent() order: OrderEntity): Promise<number> {
const users = await this.usersService.findAllByOrderId(order.id);
return users.lenght; // i can't use like this: order.users.lenght, couse it's still undefined
}
}
And there are i call userService.findAllByOrderId method two times, because i can't use order.users from context in this method, as its still undefined
So how can i write one #ResolveField method for both fields: order.users and order.usersCount, or how to call usersCount method when order.users are already existing?
Thank's a lot for any answers!
I call one method 2 times, instead 1, how can i optimize it?
Do you really need to do a query to get the number of the noOfUsers ?. Cant you just call the async users function that returns UserEntity[] and then get the length of the array that is returned .length
or
If you really need to return the noOfUsers as an int then you can create a wrapper object and put the 2 fields in that e.g.
export class UserResponse {
users: Array<UserEntity>;
noOfUsers: int
}
Then you can only have 1 method that returns both fields:
#ResolveField('users', () => [UserResponse])
async users(#Parent() order: OrderEntity): Promise<UserResponse> {
const userEntities: Array<UserEntity> = await this.usersService.findAllByOrderId(order.id);
const response: UserResponse = {
users: userEntities,
noOfUsers: userEntities.length
}
return response;
}

NestJS IntersectionType from #nestjs/swagger does not validate the combined class fields

In following up from this question, I am trying to ensure the validation remains and works. However, my combined class does not validate the included fields.
For instance, I have a basic AdminCodeDTO that sepcifies the AdminCode is required, has a valid value (1-999)
import { IsNumber, Min, Max, IsDefined } from '#nestjs/class-validator';
import { ApiProperty, ApiResponseProperty } from '#nestjs/swagger';
export class AdminCodeDTO {
#ApiProperty({
description: 'Sweda Administration Code used for time tracking that is not part of a mantis.',
})
#ApiResponseProperty({ example: 5 })
#IsDefined() #IsNumber() #Min(1) #Max(999) public AdminCode: number;
constructor(AdminCode?: number) {
this.AdminCode = AdminCode;
}
}
Testing this class works, and the validation will return the errors:
import { validate } from '#nestjs/class-validator';
import { ValidationError } from '#nestjs/common';
import { AdminCodeDTO } from './admin-code-dto';
describe('AdminCodeDto', () => {
let TestDTO: AdminCodeDTO;
beforeEach( () => {
TestDTO = new AdminCodeDTO(5);
});
it('should be defined', () => {
expect(TestDTO).toBeDefined();
});
it('should have the AdminCode value set', () => {
expect(TestDTO.AdminCode).toBe(5);
});
it('should allow creation with an empty constructor', () => {
expect(new AdminCodeDTO()).toBeDefined();
});
it('should generate the DTO errors', async () => {
const DTOValidCheck: AdminCodeDTO = new AdminCodeDTO();
const Errors: Array<ValidationError> = await validate(DTOValidCheck);
expect(Errors.length).toBe(1);
expect(Errors[0].constraints['isDefined']).toBe('AdminCode should not be null or undefined');
expect(Errors[0].constraints['isNumber']).toBe('AdminCode must be a number conforming to the specified constraints');
expect(Errors[0].constraints['max']).toBe('AdminCode must not be greater than 999');
expect(Errors[0].constraints['min']).toBe('AdminCode must not be less than 1');
});
});
To then build a simple DTO combining 2 fields to do the testing, I create a description DTO as well, to add that field for this simple example.
import { IsDefined, IsString, MaxLength, MinLength } from '#nestjs/class-validator';
import { ApiProperty, ApiResponseProperty } from '#nestjs/swagger';
export class DescriptionDTO {
#ApiProperty({
description: '',
minLength: 3,
maxLength: 20
})
#ApiResponseProperty({ example: 'Sick Day' })
#IsDefined() #IsString() #MaxLength(20) #MinLength(3) public Description: string;
constructor(Description?: string) {
this.Description = Description;
}
}
I then use the IntersectionType of #nestjs/swagger, to combine the AdminCodeDTO, with a new description field for the payload.
import { IsDefined, IsString, MaxLength, MinLength } from '#nestjs/class-validator';
import { ApiProperty, ApiResponseProperty, IntersectionType} from '#nestjs/swagger';
import { AdminCodeDTO } from './admin-code-dto';
export class AdmininstrationCodesDTO extends IntersectionType(
AdminCodeDTO,
DescriptionDTO
)
{
constructor(AdminCode?: number, Description?: string) {
this.AdminCode = AdminCode;
this.Description = Description;
}
My test however, while all the columns are defined, the validation does not work.
import { AdmininstrationCodesDTO } from './admininstration-codes-dto';
describe('AdmininstrationCodesDTO', () => {
let TestDTO: AdmininstrationCodesDTO;
beforeEach( () => {
TestDTO = new AdmininstrationCodesDTO(77, 'Test Admin Code');
})
it('should be defined', () => {
expect(TestDTO).toBeDefined();
});
it('should be defined when launched without parameters', () => {
expect(new AdmininstrationCodesDTO()).toBeDefined();
})
it.each([
['AdminCode', 77],
['Description', 'Test Admin Code'],
])('should have the proper field {%s} set to be %d', (FieldName, Expected) => {
expect(FieldName in TestDTO).toBe(true);
expect(TestDTO[FieldName]).toBe(Expected);
});
// This test fails as the validation settings are not enforced. Working on any of the DTOs directly though, the validation is confirmed.
it('should generate the DTO errors', async () => {
const TestDTO: AdmininstrationCodesDTO = new AdmininstrationCodesDTO();
const Errors: Array<ValidationError> = await validate(TestDTO, );
expect(Errors.length).toBe(8);
});
});
EDIT: This also causes a problem in my Swagger UI documentation, where this method now prevents my request schemas from showing the data. When I define my fields directly in the DTO (without IntersectionType) the fields show up in the request schema for Swagger. I have the CLI functions enabled in the project.json (NX monorepo).
As found out from your GitHub Issue (thank you for that by the way) you were using #nestjs/class-validator and #nestjs/class-transformer for the validator and transformer packages. #nestjs/mapped-types uses the original class-valdiator and class-transformer packages and these packages use an internal metadata storage device rather than the full Reflect API and metadata storage, so when Nest tried to copy over the metadata from class-validator there was none found because of the use of #nestjs/class-validator, which ended up in having no metadata present for the IntersectionType request

How to use DataPersistence to use 2 States at the same time

I use Nx to create a new application using NgRx and DataPersistence.
I have generated several states in my application (example: State A, State B).
To retrieve the data via an API request to populate State A, I use in the effect associated with this.dataPersistence.fetch.
However to launch my API request, I need data contained in State B.
This is where I block.
I saw that by using the basic effects, we can use the "withLastestFrom" operator to retrieve a different state than the one currently used.
#Injectable()
export class ElementEffects {
constructor (private store: Store<any>, private apiService: ApiService) {}
#Effect()
public yourEffect: Observable<Action> = this.actions$.pipe(
ofType<yourActionClass>(ActionsEnum.YOUR_ACTION),
withLatestFrom(this.store.pipe(select(selectSomethingFromTheStore))),
concatMap(([action, selectedDateFromTheStore]) => this.apiService.doBackendCall(selectedDateFromTheStore, action.payload).pipe(
map(([resultFromTheBackendCall, selectedDateFromTheStore]) => {
// Do Stuff
},
catchError((error) => of(new FailureAction(error)))
)
),
);
}
However I do not know how to handle that with this.dataPersistence.fetch.
By opening the DataPersistent code (https://github.com/nrwl/nx/blob/master/packages/angular/src/runtime/nx/data-persistence.ts), I see that the fetch feature is already using the withLatestFrom operator. I do not see how to recover another State.
fetch<A extends Action = Action>(
actionType: string,
opts: FetchOpts<T, A>
): Observable<any> {
return this.actions.pipe(
ofType<A>(actionType),
withLatestFrom(this.store),
fetch(opts)
);
}
My question is therefore:
How to recover State B when I use a State A effect?
Hoping to be clear enough :)
Here's an example from data-persistence.d.ts
#Injectable()
class TodoEffects {
#Effect() loadTodo = this.s.navigation(TodoComponent, {
run: (a, state) => {
console.log(state); // Here you should see all the states in your app
return this.backend.fetchTodo(a.params['id']).map(todo => ({
type: 'TODO_LOADED',
payload: todo
}));
},
onError: (a, e: any) => {
// we can log and error here and return null
// we can also navigate back
return null;
}
});
constructor(private s: DataPersistence<TodosState>, private backend: Backend) {}
}
Yes fetch will return the entire state store.
So you will need to pick what part (slice) of the store you want to use:
const state1 = state['state1'];
const state2 = state['state2'];
As withLatestFrom pull the "latests" state of the store.

Dispatch Action with Observable Value

I often find myself using the following code:
export class Component implements OnDestroy {
private subscription: Subscription;
user: string;
constructor(private store: UserStore) {
this.subscription = store.select(fromUsers.getUser)
.subscribe(user => this.user = user);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
logout(): void {
this.store.dispatch({
type: LOGOUT,
payload: {
user: this.user
}
})
}
}
As you can see I need to store the user string as a member within the component to send it with my payload.
I would rather use the user string as an observable and make use of the async pipe.
How do I need to change my code to leverage the observable of the user when dispatching the action without storing it in a member variable?
You can use ngrx effects and enhance the LOGOUT command with current user.
#Effect() logoutEffect$ = this.actions$
.ofType(LOGOUT)
.withLatestFrom(this.store$)
.map(([action: Action, storeState: AppState]) => {
return storeState.getUser;
})
.map(payload => ({type: 'LOGOUT_USER', payload}))

Set State in Ajax Call Back throws error: Warning: setState(...): Can only update a mounted or mounting

I've got a fairly simple react container component that attempts to call set state in an ajax callback called from componentDidMount. The full error is:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the UserListContainer component.
the order of operations from my console.log are:
render
child-render
componentDidMount
ajax-data
[Big ol Error Message]
I started out using async/await but when I received the error I went back to callbacks with the same result. This is the relevant code:
export class UserListContainer extends React.Component<any, any>
{
constructor() {
super();
this.state = {
users: [], request: {}
};
}
//componentDidMount = async () => {
componentWillMount = () => {
console.log('componentWillMount');
//var response: Models.IUserQueryResponse = await Api.UserList.get(this.state.request);
Api.UserList.get(this.state.request).then((response) => {
console.log('ajax-data');
if (response.isOk) {
this.setState({ users: response.data, request: response.state });
}
});
}
render() {
console.log('render');
return <UserList
request={this.state.request}
users={this.state.users}
onEditClick={this.edit}
onRefresh={this.refresh}
/>;
}
Any help would be appreciated.
you cannot set state in componentWillMount because your component could be in a transitioning state.. also it will not trigger a re-rendering. Either use componentWillReceiveProps or componentDidUpdate.
Now that aside your issue is that you are calling setState in the callback from an API request. and the issue with that is you probably have unmounted that component and dont want to setState anymore.
you can fix this with a simple flag
constructor() {
super();
this.state = {
users: [], request: {}
};
this.isMounted = false;
}
componentDidMount(){
this.isMounted = true
}
componentWillUnmount(){
this.isMounted = false;
}
then in your api request you would do this.
Api.UserList.get(this.state.request).then((response) => {
console.log('ajax-data');
if (response.isOk && this.isMounted) {
this.setState({ users: response.data, request: response.state });
}
});
I think is better to use componentWillMount() instead of componentDidMount() cause you want to load the list and then set the state, not after the component was mounted.

Resources