Angular 9 - Cannot find a differ supporting object 'getData() - angular9

I am getting this error trying to bind my control to its data. Here is some relevant code.
Template.
<tree-control [nodes]="getData"></tree-control>
Component.
public getData(): Observable<Array<any>> {
const assets: any = this.service.get('url', headers);
return assets;
}
Anything I have found so far is not helping. Any idea what's wrong with my code?
Thanks

First of all, you assign a function (getData) to the nodes property. I assume you want to assign the data from getData to it instead.
Secondly, the call to this.service.get is probably not being executed. Reason for that is that you do not subscribe to, what I assume, is a http-call that returns an Observable.
To fix this, you can do the following:
export class Foo {
nodeData: Observable<any>;
constructor(
private readonly service: YourService,
) {
this.nodeData = this._getData();
}
private _getData() {
return this.service.get(...);
}
}
Inside your template you can then subscribe and unsubscribe to the data automatically by using the async pipe.
<tree-control [nodes]="nodeData | async"></tree-control>
For all that to work I assume your service.get method returns an Observable.

Related

Overriding previous observable

I have an events app which has an all-events component which lists all the created events. When the all-events component is loaded, the events are retrieved from my database.
This is the relevant code from the all-events Typescript file:
eventParams: EventParams;
getAllEventsObs: Observable<PaginatedResult<BeachCleanEvent[]>>;
ngOnInit() {
this.eventParams = new EventParams();
}
getEvents() {
this.getAllEventsObs = this.eventService.getAllEvents(this.eventParams);
console.log("getting events");
}
pageChanged(event: any) {
this.eventParams.pageNumber = event.page;
this.eventService.setEventParams(this.eventParams);
this.getEvents();
}
Here is the html from the all-events component:
<div class="background-img">
</div>
<div class="container">
<div class="events-container" *ngIf="getAllEventsObs | async as getAllEventsObs">
<div class="event-item" *ngFor="let event of getAllEventsObs.result">
<app-event-card [existingEvent]="event"></app-event-card>
</div>
<pagination *ngIf="getAllEventsObs.pagination as eventPagination"
[boundaryLinks]="true"
[totalItems]="eventPagination.totalItems"
[itemsPerPage]="eventPagination.itemsPerPage"
[(ngModel)]="eventPagination.currentPage"
(pageChanged)="pageChanged($event)">
</pagination>
</div>
</div>
The getAllEvents method is just a simple http request. I won't show this code as I know it works correctly.
This is the PaginatedResult class:
export interface Pagination {
currentPage: number;
itemsPerPage: number;
totalItems: number;
totalPages: number;
}
export class PaginatedResult<T> {
result: T;
pagination: Pagination;
}
This is the EventParams class:
export class EventParams {
pageNumber = 1;
pageSize = 4;
}
When the page initially loads, it loads correctly and displays the first four events:
The issue I am having is that when clicking on the next page, it gets stuck and won't load the next four events. No error is displayed on the console but the "getting events" console.log I created in the getEvents method above just keeps firing:
I suspect this is something to do with the way I am consuming the observable in the html code (using the async pipe). How would I go about resolving this? Is this where I should be using the switchMap RXJS operator? If so, how would I use it in this scenario?
You are on the right track... and yes, you should use switchMap :-)
Instead of re-assigning your source observable inside getEvents(), you could simply define it to depend on your params, and just push new params when they change. switchMap will execute the service method each time the params change.
But if the EventService is keeping track of the params anyway, it's probably the simplest to have it expose the events$ based on those params.
If you aren't already, define the params as a BehaviorSubject with default value and add a method for consumers to modify them. Then expose a single events$ observable that represents the events based on the specific params:
service:
private params$ = new BehaviorSubject(new EventParams());
public setEventParams(params) {
this.params$.next(params);
}
public events$ = this.params$.pipe(
switchMap(params => this.getAllEvents(params))
);
component:
events$ = this.eventService.events$;
pageChanged(params) {
// build proper params object if necessary
this.eventService.setEventParams(params);
}
Your component code becomes a lot simpler since it doesn't need to be concerned with managing the observable and keeping track of params.

Angular Template/Interpolation parse error on find/filter

Error:
Error: Template parse errors: Parser Error: Bindings cannot contain
assignments at....
line: <div>Closed: {{blah}}.find()...
HTML:
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px">
<div>Total: {{(issuesData$ | async)?.length}}</div>
<div>Closed: {{(issuesData$ | async)?.filter(data => {data.closedOn}).length}}</div>
</div>
I'm curious if it is possible to use find/filter without running into the template parse error when find/filter are called on a collection in the interpolation statement.
EDIT: Angular 2 - Bindings cannot contain assignments - This does not work for me because I'm passing in an Observable to the component. OnInit I assign the #Input data variable to the issuesData$ observable. Using something like {{getCount()}} in the interpolation results in no data. I tried implementing that like this:
ANGULAR:
#Input()
data;
ngOnInit() {
this.issuesData$ = this.data;
}
getCount(){
this.issuesData$.subscribe(data => {
return data.length;
})
}
HTML:
<div>Total: {{getCount()}}</div>
But there is nothing to subscribe to when the getCount() is called in the interpolation and this doesn't work either {{(getCount() | async}}
In this scenario you should use map and format the response in a way that makes the data easily consumable by the component/template.
Also you can't return a value from subscribe, you can assign a value though.
find returns one match, filter returns an array. You meant to use filter if you are wanting to see the total number of matches using length
If you use shareReplay the observable will be resolved once so it can be called multiple times using either async pipes or subscribe without incurring additional costs if it does something with external resources like making an http request.
Finally you should define types / definitions, typescript is (can be) strongly typed and this is a big advantage over javascript (what it supersedes).
Template.html
<div>Total: {{(issuesData$ | async)?.length}}</div>
<div>Closed: {{(issuesData$ | async)?.closedOnLength}}</div>
YourComponent.ts
import { Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
#Input()
data: Observable<{closedOn:any}[]>;
issuesData$: Observable<{length: number, closedOnLength: number}>;
ngOnInit() {
this.issuesData$ = this.data.pipe(map((_) => {
return {
length: _.length,
closedOnLength: _.filter(d => d.closedOn).length
};
}), shareReplay());
}
count: number;
readCount() {
// you can't return data in a subscribe callback, it is asynchronous
// you can assign a value though
this.issuesData$.subscribe(_ => this.count = _.length);
}
You should subscribe to your Observable and manually assign the variables you want in there instead, purely to reduce the complexity of your template code.
#Input()
data;
total: number;
closed: number;
ngOnInit() {
this.issuesData$ = this.data;
this.issuesData$.subscribe(next => {
this.total = next.length;
this.closed = next.filter(x => x.closedOn).length;
}
}
Then just use the total and closed variables in your template.
To call find or filter from the async array in the template, define the callback in the component class. For example, you can set the method isClosedOn as the filter callback:
<div>Closed: {{ (issuesData$ | async)?.filter(isClosedOn).length }}</div>
and define it as follows in the component class:
public isClosedOn = data => data.closedOn;
See this stackblitz for a demo.

Consuming IMDB api results bad json

I have a simple program that consumes IMDB api, I'm getting the result, but it was shown as error because the result is not a structured json.
MovieService.ts
export class MovieService {
constructor(private http:HttpClient) { }
getMovie(movie:string){
return this.http.get(this.generateURL(movie));
}
private generateURL(movie:string){
return "https://v2.sg.media-imdb.com/suggests/titles/"+movie.charAt(0)+"/"+movie+".json?callback=imdb$"+movie;
}
}
addmovie.component.ts
private _filterMovies(value: string) {
this.movieService.getMovie(value).subscribe(
movies => {
console.log(movies);
return movies;
}
);
}
ngOnInit() {
this.addMovieForm.get('movie').valueChanges.subscribe(val => {
this._filterMovies(val)
});
}
I'm getting error like
the response is of bad json. How can I format the json upon receiving? How to solve this? Any leads would be helpful.
The result is not JSON, but rather JSONP. It is essentially returning you a script that is trying to execute the callback method specified.
Instead of http.get() you should call http.jsonp(url, "imbdIgnoresThisParam").
However, according to this answer, the callback query string parameter is ignored by IMDB. The answer suggests dynamically creating the expected callback function, whose name contains the title for which you are searching. In that callback you could do a few different things.
Use the closure to call / set something in your MovieService. This will result in your call to the API throwing an error, as the Angular framework's callback will not be called as expect. You could ignore the error.
Try to call the expected Angular callback, ng_jsonp_callback_<idx>. This will prevent the API call from throwing, but it may not be reliable. The callback name is dynamic and increments with each jsonp() call. You could try to track the number of jsonp() calls in your app. And of course, the framework may change and break this solution. Concurrent calls to getMovie() may break, as the next one may step on the previous callback on the window. Use with caution!
In typescript, your getMovie() function and related helpers might look like so:
private imdbData: any = null;
private jsonpIdx = 0;
private setImdb(json: any) {
this.imdbData = json;
// or do whatever you need with this
}
getMovie(movie:string) {
// dynamically create the callback on the window.
let funcName = `imdb$${movie}`;
window[funcName] = (json: any) => {
// use the closure
this.setImdbData(json);
// or try to call the callback that Angular is expecting.
window[`ng_jsonp_callback_${this.jsonpIdx++}`](json);
}
// go get your data!
let url = this.generateURL(movie)
return this.http.jsonp(url, "ignored").subscribe((json) => {
// this happens if you successfully trigger the angular callback
console.log(json);
}, (err) => {
// this happens if the angular callback isn't called
console.log(this.imdbData); // set in closure!
});
}
Edit for Angular 4
For Angular 4, it looks like you will need to import the JsonpModule along with the HttpModule. Then, you'd inject jsonp just like you'd inject http into your service. The call to IMDB becomes this.jsop.request(url).subscribe(...) and your dynamic callback name needs to change, too.
window[funcName] = (json: any) => {
// or try to call the callback that Angular is expecting.
window["__ng_jsonp__"][`__req${this.jsonpIdx++}`]["finished"](json);
}
I don't have an Angular 5 or 6 project immediately set up, so hard to say if there are any differences with the callback in those versions.
Sort of a hack, but hope it helps!

How should I parse this json object in react with lifecycle method?

How should I parse this using lifecycle methods?
{"blocks":[{
"key":"33du7",
"text":"Hello there!",
"type":"unstyled",
"depth":0,
"inlineStyleRanges":[],
"entityRanges":[],
"data":{}}],
"entityMap":{}
}
I want to render the text in my component but I don't know why it throws undefined error. How should I call it?
This is my component:
class Blog extends Component{
constructor(props){
super(props);
this.blogContent = props.blogContent;
this.blogId = props.blogId;
this.handleRemoveBlog = this.handleRemoveBlog.bind(this);
this.state = {
blog__: '',
};
}
handleRemoveBlog(blogId){
this.props.removeBlog(blogId);
}
This is my lifecycle method , I would use this.setState but first of all it's giving undefined in console.
componentWillMount(){
this.state.blog__ = JSON.parse(this.blogContent);
console.log(this.state.blog__.text); // this gives undefined
}
This is the render part..
The data is coming from Firebase.
And {this.blogcontent} gives that json string that I previously mentioned.
render(props) {
return(
<div className = "blog header">
<p>{this.blog__.text}</p>
</div>
);
}
}
Blog.proptypes = {
blogContent: Proptypes.string
}
This would mostly depend on where you are getting this object from. If it is fetched over the network then the best place to pass it is in the componentDidMount. The reason for this is that the alternative lifecyle method (componentWillMount) does not guarantee a re-render of your component since it does not wait for async actions to finish execution before passing control down to your render method. Hence componentDidMount is best because as soon as new props are received or state is changed it will trigger a re-render. However, if this object is pulled from within the application then chances are, it will work just fine even if pulled within componentWillMount. This is because that operation would be much quicker, so much that control would be passed down to the render method with the new props. This is not guaranteed especially if you want to set state in the process (setting state is also async, so control might execute the rest of the code before all the required data is received).
In short, pass this to componentDidMount and in your render function, before accessing this prop, make sure that it exists. That is, instead of
render() {
return <div>{this.props.theObject.blocks[0].key}</div>
}
rather do:
render() {
return <div>{this.props.theObject && this.props.theObject.blocks[0].key}</div>
}
This is how you would do it (assuming you are getting the file over the network using axios)
componentDidMount() {
axios.get('url/to/the/file')
.then(fileData => this.setState({
data: fileData
});
}
render() {
// return whatever you want here and setting the inner html to what the state holds
}
You should not modify the state using
this.state.blog__ = JSON.parse(this.blogContent);
The proper way to do it is using the this.setState() method:
this.setState({blog__: JSON.parse(this.blogContent)})
Then, to ensure that the component will be re-rendered, use the method shouldComponentUpdate():
shouldComponentUpdate(nextProps,nextState) {
if(nextState != this.state) {
this.forceUpdate()
}
}
Take a look at the State and Lifecycle docs.
Other point: Use componentDidMount() instead of componentWillMount(), because it will get deprecated in the future.
Atention: The setState() is an asynchronous method. So, it won't instant update your state.
Use this.setState({}) in your componentWillMount function instead assign the data to the variable. Also I recommend to use componentDidMount instead of componentWillMount because it's getting deprecated in the future.
componentDidMount(){
let text = JSON.parse( this.blogContent );
this.setState({blog__: text });
}
Edit: Only use setState in componentDidMount according to #brandNew comment

How to parse Date object properly of TypeScript class when HttpClient mapping won't?

Task.ts:
export class Task {
name: string;
dueDate: Date;
}
tasks.service.ts:
#Injectable()
export class TasksService {
constructor(private http: HttpClient) { }
getTasks(): Observable<Task[]> {
return this.http.get<Task[]>(`${WEBAPI_URL}/Tasks`);
}
}
The Task objects I get back from getTasks() have their dueDate field assigned but the value is of type string instead of Date like I would expect.
Some searching lead me to this issue on the Angular github which made clear to me that HttpClient has no intent of properly parsing my object. Unfortunately the issue didn't give helpful guidance about what I should actually be doing to get my Date object. What do I do?
You have several options here.
1) You can deal with the date as a string in the UI. So change the definition of Task to retain the date as a string and work with it that way, converting it to a date for calculations as needed.
2) You can map each object coming back from the Http request to a Task object. Something like this:
getTasks(): Observable<Task[]> {
return this.http.get<Task[]>(`${WEBAPI_URL}/Tasks`)
.pipe(
map(items => {
const tasks: Task[] = [];
return items.map(
item => {
item.dueDate = new Date(item.dueDate);
return Object.assign(new Task(), item);
});
}),
tap(data => console.log(JSON.stringify(data))),
catchError(this.handleError)
);
}
This also have the benefit of having actual task objects in your array, meaning that if you ever add Task methods or getters/setters they will be correctly associated with your tasks array.
EDIT:
It may be better to build a utility class that handled the serialization/deserialization of your objects. Then the above code would look more like this:
getTasks(): Observable<Task[]> {
return this.http.get<Task[]>(this.url)
.pipe(
map(TaskSerializer.serialize),
catchError(this.handleError)
);
}
declare it as a date in the component like this:
example.component.ts
constructor(private taskService: TaskService) {
}
ngOnInit() {
this.taskService.getTaksks().subscribe(response => {
tempValue = response.body;
tempValue.dueDate = new Date(tempValue.dueDate.format('MM-DD-YYYY');
});
}
OR save it as an instant
Task.ts
export class Task {
name: string;
dueDate: Instant;
}
I would suggest doing the first way. I would also suggest looking at the moment.js library
EDIT: I would declare it as a Date object and let it store it as a string on the db. That is how I have seen most use cases for dealing with dates and it is how everyone in my company has dealt with dates. 99% of the time you just want the month/day/year so it makes sense that you will store only that on the db, but it is a little cumbersome to format it to a date object on the ui side.
I found a not so heavy interceptor, allowing to have directly correct Date in objects when using HTTP calls in Angular. You can find it here: https://dev.to/imben1109/date-handling-in-angular-application-part-2-angular-http-client-and-ngx-datepicker-3fna
Please note that the only thing I had to change was the regex, in order to make the Z at the end optional and I use the DateTimeFormatter.ISO_DATE_TIME format on server side.