How to access variable defined in *ngFor? - javascript

In my first Angular 4 application, I defined a list component :
<edm-document *ngFor="let document of documents" class="column is-one-quarter"></edm-document>
Document is an interface :
export interface Document {
id?: Number,
name: string,
filePath: string
}
All is working as expected, i.e I get my documents list. But now I would like to access to document variable inside my DocumentComponent (the edm-document tag component)
Inside my DocumentComponent template, if I try this it's not working :
<p>{{ document.name }}</p>
I get this error : DocumentComponent.html:1 ERROR TypeError: Cannot read property 'name' of undefined.
I need to enforce document definition like this, and specify document as an input :
<edm-document *ngFor="let document of documents" [document]="document" class="column is-one-quarter"></edm-document>
Now it works but seems a bit redundant to me as I defined a let in loop. Does that mean the variable defined with let is only available in tag where ngFor directive is set ?
Am I missing something?
Thanks,
Nicolas

it works but seems a bit redundant to me as I defined a let in loop
It is not as redundant as it might seem, which becomes obvious when rewriting things a bit:
When not explicitly defining what the component should use (with [document]="document" in your example) then how would your component know that the parent variable is named document? Consider:
<edm-document *ngFor="let d of documents" [document]="d"></edm-document>
One could argue that Angular could introduce some parent variable to access the outer loop variable, but then the component would know how it's going to be used, and could only be used in a loop. Reusable components should not be aware of that.
How would it know that it can use that loop variable directly, and does not need some child property instead? Like:
<edm-document *ngFor="let d of documents" [document]="d.text"></edm-document>
So: your code is just fine.

Initially during DOM rendering the documents object will undefined
Use a typesafe ? operator
<p>{{ document?.name }}</p>
Use a *ngIf with a array length condition as below,
<span *ngIf="documents.length > 0">
<edm-document *ngFor="let document of documents" [document]="document"
class="column is-one-quarter"></edm-document>
</span>

well you can also do something like this
<edm-document *ngFor="let document of documents" class="column is-one-quarter">
<span class="something">{{document.name}}</span>
</edm-document>
and in the edm-document.component.html do something like
<ng-content select=".something"></ng-content>

The value (document) of the loop is valid inside of that block where the *ngFor placed. In your case between: <edm-document>..</edm-document>
In your example:
<edm-document *ngFor="let document of documents"class="column is-one-quarter">
<p>{{ document.name }}</p> <!-- document.name is valid -->
</edm-document>
<p>{{ document.name }}</p> <!-- document.name is invalid -->

Related

How to use variable inside variable in angular template?

I am new to angular , can someone tell me if I can use variable inside variable in angular.
Explaination:
I am creating one dropdown input component where it will make api call to get data.
There is an #Input() selector:string = "" which will tell what to select from data
Inside template it will run *ngFor loop, then inside html I want to display as kind of like that:
<option *ngFor="let item of data" [value]="item.id">
{{ item.{{selector}} }}
</option>
In other module it will be used as:
In one module <app-input [selector]="'name'"></app-input>
Another one. <app-input [selector]="'id'"></app-input>
How Can I use selector inside this any way?
{{ item[selector] }}
use the bracket syntax for accessing a property with a variable key.

Angular modular object property binding to ngModel

I have an object with 1 set properties and X modular ones which are added depending on at array, the array elements are the object names. This works perfectly to display the elements in my table. My issue comes when I want to bind the textboxes to my model, as I am getting an error saying the object properties does not exist. I have included an *ngIf to check if the property is there and in theory it finds it.
This is part of my HTMl code trying to use one of the modular property names, I have used one which I know will exists to simplify the debugging, next step will be to change this to be fully modular:
<ng-container *ngIf="newWaste[0].hasOwnProperty('Metal')">
<ng-container [matColumnDef]="column" *ngFor="let column of displayedColumns | slice:2 let i = index">
<th mat-header-cell *matHeaderCellDef>{{ displayedColumns[i+2] }}</th>
<td mat-cell *matCellDef="let emp">
<input type="text" matInput placeholder="" value="{{ emp[column] }}" [(ngModel)]="newWaste[0].Metal" />
</td>
</ng-container>
</ng-container>
I have also tried counting the number of properties instead of directly checking if it exists, no difference neither.
This is my error:
src/app/pages/waste/containers/waste-page/waste-page.component.html:33:132 - error TS2339: Property 'Metal' does not exist on type '{ measuredDate: Date; }'.
33 <input type="text" matInput placeholder="" value="{{ emp[column] }}" [(ngModel)]="newWaste[0].Metal" />
~~~~~
src/app/pages/waste/containers/waste-page/waste-page.component.ts:19:16
19 templateUrl: './waste-page.component.html',
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Error occurs in the template of component WastePageComponent.
I create the object with Date parameters, the rest added later.
Binding to Date works perfectly.
Any suggestions?
I create the object with Date parameters, the rest added later
Here's the problem. Initialize newWaste with Metal property already there (no problem if it's initialized to undefined), and probably adjust the TS typing of newWaste so Metal property is non-optional. Angular objects to ngModel pointing at a property which potentially doesn't exist.
Thinking about the whole initialization issue, I have tried to initialize an empty array, instead of an array with the object including the "measuredDate" property.
So instead of this:
public newWaste = [{measuredDate: Date}];
I am just using this, and add all properties in the code:
public newWaste = [];
ngOnInit(): void {
//add the measureDate property
this.newWaste.push({measuredDate: new Date()});
//in other methods I add the more specific properties such as Metal
}
It workpes perfectly the way like that.

How to loop over object properties with ngFor in Angular app

I can write data by keyValuePipe or {{data | JSON}} but I think that is not good solution for me. How I can write data differently? How I use for this example Object.entries, Object.keys or Object.value?
Code with JSON in Stackblitz: https://stackblitz.com/edit/angular-ivy-jn8snh?file=src/app/app.component.ts
Your object is considered as a dictionary in javascript.
You can loop over properties of your object as follow:
<div *ngFor="let property of data">
<p>{{ property }} - {{ data[property] }}</p>
</div>
You can't to do that out of the box. NgForOf allows iterating over a collection, and by default object does not implement required methods and properties. In theory you could get the keys of the object by writing something like this:
<p *ngFor="let property of Object.keys(object)">
{{property}} {{object[property]}}
</p>
But the way the contents of ngFor are evaluated, the global variable Object is not present in the scope. So you're best off writing a custom pipe that would do the work for you by transforming your object into iterable of your choice that is suitable for your requirements (not idea what that would be since the only information is that "keyValuePipe or {{data | JSON}} is not good solution for me" without any reasoning).

JSrender: cannot access parent variable inside if condition

as the title says, the problem is clear:
within the construction:
{{if is_completed == 1}}
<div>
<p>{{:#parent.parent.data.myproperty}}</p>
</div>
{{/if}}
the parent properties are not visible!
I solved it by creating construction like:{{for movies ~myproperty=myproperty}} in parent loop, and ~myproperty is visible inside IF conditions, but what if I have several variables, what if I have many-level nesting data structure?
The {{if ...}} block adds another view, so means you need to add a .parent to step up through that view, as in: {{:#parent.parent.parent.data.myproperty}}
You can pass in variables as you said (~myproperty=...) and they will be visible to any depth of nesting.
Your variable can be an object too such as the current data object: ~myObj=#data:
{{sometag a=b ~myObj=#data}}
....
{{:~myObj.myproperty}}
...
{{/sometag}}
so you don't need a separate variable for each property.
You can also access the top-level data object and drill down from there:
{{:~root.foo...myproperty}}.
And finally you can use #get("item") to step up through any number of {{if}} blocks and get the nearest "item" view (i.e. the item view for a repeating {{for ...}} block).
So you would write:
{{:#get("item").data.myproperty}}

Pass parent scope value into ng-repeat loop in Angular

This should be an extremely simple question, but all of the workarounds I've found are complex. I'm looping through an array of objects in using ng-repeat in a template as follows:
<div class="row-fluid" ng-repeat="message in messages.current|filter:'draft'">
{{ message.subject }} ... {{ campaign.name }} ...
</div>
Since the ng-repeat creates a new scope, the 'campaign' object from the controller doesn't seem to be accessable. Is there any way (aside from adding the campaign object to every item in my array) of getting that value?
Thanks in advance.
You can access the parent scope by using $parent
<div class="row-fluid" ng-repeat="message in messages.current|filter:'draft'">
{{ message.subject }} ... {{ $parent.campaign.name }} ...
</div>
This is a way that works that doesn't use $parent. It searches upwards through the nested scopes to find the object you're using, however many scopes it has to go through.
In the scope that contains the list, you can define an object with the list as a property, like this:
$scope.obj = {};
$scope.obj.items = ['item1','item2','item3'];
Then have the ng-repeat look like this:
<div ng-repeat="item in obj.items | filter:'item3' track by $index">
{{obj.items[ obj.items.indexOf(item) ]}}
</div>
(you need to use obj.items[ obj.items.indexOf(item) ] rather than obj.items[ $index ] because $index is the index of the filtered array, not the original)
The reason this works is because while obj doesn't exist in the current scope, as you attempt to access its property, Angular will look above the current scope rather than give you an error (if you just tried {{obj}} it would be undefined, and Angular would be happy with giving you nothing instead of looking through higher scopes). This is a helpful link about nested scopes: http://www.angularjshub.com/examples/basics/nestedcontrollers/
In my case I needed the track by $index, because I had an input with ng-model bound to an item in the array, and whenever the model updated, the input would blur because I think the HTML was being re-rendered. A consequence of using track by $index is that items in the array with identical values will be repeated. If you modify one of those other than the 1st one, weird things will happen. Maybe you can filter for uniqueness to avoid that.
I'm relatively new to AngularJS, so please comment if there is anything big I'm missing. But this works, so I'm using it at least.
Another method might be to pass parent scope as a scope variable to the directive i.e.
<my-directive
md-parent-scope="this"
ng-repeat="item in items"></my-directive>
It's a bit messy, but you have more control over what the parent actually is and can pass anything in.

Categories