I'm trying to use *ngIf to show and hide an element based on a member of the class its built on. But I keep encountering the following error despite being able to use other data members to display titles, etc.
TypeError: Cannot read property 'hidden' of undefined
My html looks like
...
<md-list-item *ngFor="let navigation of flatNavList"
(click)="onSelect(navigation)" *ngIf="navigation.hidden==false">
{{navigation.title}}
</md-list-item>
...
with a navigation array in my *component.ts file defined as
...
flatNavList: Navigation[];
...
and the Navigation class looking like
export class Navigation {
constructor(
public name: string,
public title: string,
public icon: string,
public location: string,
public hidden: boolean,
public roles: string[],
public children: Navigation[]
) { }
}
If I remove the *ngIf everything renders fine. Why can I use navigation.title to display the name, yet I can't use navigation.hidden to toggle whether or not the element is displayed?
EDIT------------------------------
[hidden]="navigation.hidden"
But several articles say that this is bad practice in Angular 2
It appears that navigation is undefined sometimes.
So first test for it and then check its hidden property:
*ngIf="navigation && !navigation.hidden"
Related
I have a templated component (a tooltip) that has a parameter and passes that parameter in context to child content. That parameter is a wrapper for ElementReference. The purpose of this is to get back to the tooltip the child's reference, once it is set.
What I want to do is to store a particular instance of that tooltip component in a reusable RenderFragment in several places.
But I get the error The name 'context' does not exists in the current context.
This is original question, but it proved to be oversimplified. Please go to second separation line, with sample more resembling my situation.
Here is a sample (not that tooltip, but simplified code that has exactly the same problem).
Templated component RenderFragTempl:
#inherits ComponentBase
<span id="#(Ref)">
#IntChildContent(Ref)
</span>
#code {
[Parameter] public RenderFragment<int> IntChildContent { get; set; }
[Parameter] public int Ref { get; set; }
}
and the call in Index.razor:
#page "/"
#using BlazorServer.Pages.StackOverflow
<RenderFragTempl Ref="10">
<IntChildContent>
<p>#context</p>
</IntChildContent>
</RenderFragTempl>
<br />
#test
#code {
//compiler error CS0103: The name 'context' does not exist in the current context
private readonly RenderFragment test = #<RenderFragTempl Ref="10001">
<IntChildContent>
<p>#context</p>
</IntChildContent>
</RenderFragTempl>;
}
EDIT: 1
I have a tooltip component that has a child content. Tooltip will be shown whenever the mouse hovers over that child content. But tooltip does not wrap the child content with anything. So if you check out the source, you will only see the child content without any indication of the tooltip. The moment mouse cursor hovers over the child content, the tooltip container is added to the bottom of the page and is position right over that child content. This is quite problematic to achieve in blazor because the tooltip needs to have the reference to the child content. But references are established after parameters are filled. So a special wrapper is used to achieve that and Tooltip is built as a templated component.
So the wrapper for ElementReference I use (curtesy of MatBlazor):
public class TargetForwardRef
{
private ElementReference _current;
public ElementReference Current
{
get => _current;
set
{
Set(value);
//this is just for debugging purpose
Console.WriteLine($"Ref: {value.Id ?? "null"}");
}
}
public void Set(ElementReference value) => _current = value;
}
My simplified Tooltip as RenderFragTempl (just the important bits)
#inherits ComponentBase
<span>
#UnboundChildContent(RefBack)
</span>
#code {
[Parameter] public RenderFragment<TargetForwardRef> UnboundChildContent { get; set; }
[Parameter] public TargetForwardRef RefBack { get; set; } = new TargetForwardRef();
}
And my index.razor
#page "/"
#*
//this is working, will printout to console `<span>` reference id, you will be
//able to find it once you go to source; I added this here for reference
*#
<RenderFragTempl>
<UnboundChildContent>
<span #ref="#context.Current">Span content</span>
</UnboundChildContent>
</RenderFragTempl>
<br/>
#test
#code {
//compiler error CS0103: The name 'context' does not exist in the current context
private readonly RenderFragment test =
#<RenderFragTempl>
<UnboundChildContent>
<span #ref="#context.Current">Hover to show tooltip</span >
</UnboundChildContent>
</RenderFragTempl>;
}
To answer the question - why I am trying to use RenderFragment - imagine I have a collection of cards - let's say 150. The card component accepts as a parameter IList<RenderFragment> for rendering buttons on a card. I want to pass my icon with tooltip to that card. I need to have access to Tooltip's context.
I tried renaming context to something else, but I get the same error (except in the error there is new context name).
Why define a templated component and use it like that ?
However, here's code sample that demonstrates how to get the same required result without defining a templated component:
#test(12)
#code {
private RenderFragment<int> test => (value) => (__builder) =>
{
<span id="#(value)">
#value
</span>
};
}
But if you insist, here's the code that demonstrate how to render your component:
#test((10001, 15))
#code
{
private RenderFragment<(int, int)> test => (value) => (__builder) =>
{
<RenderFragTempl Ref="#value.Item1">
<IntChildContent>
<p>#value.Item2</p>
</IntChildContent>
</RenderFragTempl>;
};
}
Update:
The following code describes how to render the templated component with the internal variable context. You can improve on it as you like.
Note: I did not read your update... I just do what you requested on the original question, but this time using context.
#test(121)
#code {
private RenderFragment<int> test => (value) => (__builder) =>
{
__builder.OpenComponent<TemplatedComponent>(0);
__builder.AddAttribute(1, "Ref", value);
__builder.AddAttribute(2, "IntChildContent",
(RenderFragment<int>)((context) => (__builder2) =>
{
__builder2.OpenElement(3, "p");
__builder2.AddContent(4, context);
__builder2.CloseElement();
}
));
__builder.CloseComponent();
};
}
Note that when you invoke the RenderFragment delegate you pass it a value which is assigned to the Ref parameter, and is passed in the form the the internal context variable
I asked this question in https://github.com/dotnet/aspnetcore/issues/29671 and I got an answer. This is not exactly what I wanted, but that seems to be official:
private readonly RenderFragment<TargetForwardRef> test => context =>
#<RenderFragTempl>
<UnboundChildContent>
<span #ref="#context.Current">Hover to show tooltip</span >
</UnboundChildContent>
</RenderFragTempl>;
I have a custom HTML tag <fancy-foo> that I would like to extend the functionality of. Some of the <fancy-foo> elements would be extended using the pretty-bar custom element, so that I can use them in my HTML as
<fancy-foo is="pretty-bar">
<!-- content -->
</fancy-foo>
so, I define classes FancyFoo and PrettyBar, define the fancy-foo element and extend it using pretty-bar, like so:
class FancyFoo extends HTMLElement {
constructor() {
super();
}
}
class PrettyBar extends FancyFoo {
constructor() {
super();
}
}
window.customElements.define('fancy-foo', FancyFoo);
window.customElements.define('pretty-bar', PrettyBar, {extends: 'fancy-foo'});
Sadly, Google Chrome spits out the error
Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': "fancy-foo" is a valid custom element name
That's an odd error. Strangely, it says "fancy-foo" is a valid custom element name; I know that, but why should that throw an error?
What's going on here?
You cannot extend Custom Elements that way (with {extends: '...'}). You can only extend buit-in (= standard) HTML elements.
If you want to design PrettyBar as an extension of FancyFoo, you'll need to define it as an autonomous custom element:
class PrettyBar extends FancyFoo {...}
window.customElements.define('pretty-bar', PrettyBar)
Same thing for the HTML tag:
<pretty-bar></pretty-bar>
This way it is possible to create correct inheritance chain. Extended built-in element's class is further extensible:
class FancyFoo extends HTMLDivElement {
constructor() {super();}
}
class PrettyBar extends FancyFoo {
constructor() {super();}
}
window.customElements.define('fancy-foo', FancyFoo, {extends: 'div'});
window.customElements.define('pretty-bar', PrettyBar, {extends: 'div'});
<div is="fancy-foo"></div>
<div is="pretty-bar"></div>
customElements.define determines the is-name, constructor, and built-in's tag-name, if used this way. It does not need to know the exact constructor chain, provided it leads to the call of the appropriate built-in's constructor. You can establish the inheritance by extending one class by another.
Style it to override the div behavior, or use a more appropriate built-in element as base.
I'm doing the following (it's working as expected) in my parent component.
<app-textbox [info]="{caption:'Boink',value:'Oink'}"
... ></app-textbox>
In the receiving child component I have the following declaration.
#Input() info: any;
Now I want to improve the code and make it hard-typed, so I introduced and imported the following class.
export class TextBoxInfo { constructor(public caption: string, public value: string) { } }
Then, I updated the child component's input as follows.
#Input() info: TextBoxInfo;
Everything still works, as expected but I also wanted to improve the markup in HTML by switching to the following syntax.
<app-textbox [info]="new TextBoxInfo('Boink','Oink')"
... ></app-textbox>
That doesn't work and I'm getting the error message .
Uncaught Error: Template parse errors:
Parser Error: Unexpected token 'TextBoxInfo' at column 5 in [new TextBoxInfo('Boink', 'Oink')]
in ng:///AppModule/ParentComponent.html#45:24 ("
/div>
app-textbox [ERROR ->][info]="new TextBoxInfo('Boink', 'Oink')" ...
I've try to google to confirm or contradict that I can use the syntax like new Something(...) in the template's markup. Nothing conclusive this far. I also tried to google for the error but it's simply telling me that the syntax isn't recognized. I haven't found any viable examples of how to create an object and pass it in the template and googlearching it is complicated by the lack of good key words.
Am I approaching the object creation incorrectly?
Using type literals in templates is not supported. The scope of a template is the component instance, and therefore only properties of the component instance can be accessed.
If you need to reference identifiers outside of that scope, you need to move the code/expression to the components class and expose it to the template from there.
class MyComponent {
createTextBoxInfo(p1, p2):TextBoxInfo { return new TextBoxInfo(p1, p2); }
}
[info]="createTextBoxInfo('Boink','Oink')"
while this concrete case is a bad example in practice.
It would create a new TextBoxInfo every time change detection is run which is probably not what you want and will bring the performance of your app to its knees.
It's better to assign the value to a property and bind to that instead:
class MyComponent {
textBoxInfo = new TextBoxInfo('Boink','Oink'); }
}
[info]="textBoxInfo"
i have a json data like :
{"extra":"main menu",
"menu":[
{"id":1,"name":"Menu 1"}
,{"id":2,"name":"Menu 2"}
,{"id":3,"name":"Menu 3"}
,{"id":4,"name":"Menu 4"}
,{"id":5,"name":"Menu 5"}
,{"id":6,"name":"Menu 6",
"menu":[
{"id":7,"name":"Menu 7.1"},
{"id":8,"name":"Menu 7.2"},
{"id":9,"name":"Menu 7.3"}
]},
{"id":10,"name":"Menu 8 "},
{"id":12,"name":"Menu 9 "},
}
i have a class menu like that :
export class Menu {
constructor(public id?: number , public name?: string ){}
}
and a parser like :
export class ParserMenu {
constuctor(public module?: string , public menu?: Menu[])
}
i can get data and parse it correctly but the problem is when i have a menu inside a menu , i don't know how to parse this data so , what is the solution to solve that ?
I suggest you use interfaces instead of classes, I see no need for using a class here ;) So it would look like this:
export interface Menu {
id: number;
name: string;
menu?: Menu[]
}
Then you just simply cast your JSON to this interface when retrieving data (I assume it's coming from backend) by extracting the data from menu:
Service:
getData(): Observable<Menu[]>{
return this.http.get('theUrl')
.map(res => res.json().menu)
}
and in component:
menues: Menu[];
this.service.getData()
.subscribe(data => {
this.menues = data;
})
Now you have data of type Menu. Worth mentioning is, that interfaces do not exist during runtime, as there is no interfaces in JS. Interfaces are more of a help for the programmer and IDE. IDE can warn you during compile time if you are trying to do something wrong, like assigning wrong type for properties.
JSON.parse() will not instantiate any of your classes. So you'd better use interfaces instead of classes to model the structure of your JSON.
But if you have a menu inside a menu, then... that menu will be inside the menu in the object created by JSON.parse(), too. You access it using result.menu[5].menu.
And you thus need a menu? attribute, of type Array<Menu>, inside Menu.
So here is the problem, I am attempting to create a new component from within a service that is injected within the App Component. I need the new component to be placed within the app component html tag not outside. The thing is I really do not want the app component to have to provide anything to the service I may need to inject the service into other places and hence not have it tightly coupled to the app component. So far I have created a DIV at the end of the app component html and then used #ViewChild to read the ViewContainerRef from this element located within the app component. This is then provided to the service via a function call so that it can make use of createComponent. This allows for the NEW component to be placed within the scope of the app component, not within the body. Unfortunately this is too dependant on the app component providing the ViewContainerRef. Any ideas of how I can create the new component as described.
Code Example
app.component.html
<app-component>
<div #newCompHook></div>
</app-component>
app.component.ts
#ViewChild('newCompHook', {read: ViewContainerRef}) newCompViewRef: ViewContainerRef;
constructor(appService: AppService) {
appService.setViewRef(this.newCompViewRef);
}
app.service.ts
private myViewRef;
constructor(private compiler: ComponentResolver){
this.myViewRef = null;
}
public setViewRef(vr: ViewContainerRef): void {
this.myViewRef = vr; // <-- DO NOT WANT TO DO THIS !!!
}
public createNewComp(childCmp: Type): void {
if (this.myViewRef !== null){
this.compiler.resolveComponent( childCmp ).then((compFactory:ComponentFactory) => this.myViewRef.createComponent(compFactory) )
}
}
createNewComp is called by an external source and may or may not provide the childCmp type to be resolved.
So any ideas of how I can do this without needing to provide anything from the app component ???
If you need to have the viewContainerRef in your service that is the only solution...
But it is not a good practice to generate HCI components in a service. It's the role of other components.
Let's take an exemple : your server send you a list of objects (a list of strings for exemple) and you want to generate a button for each string.
In your service you just manage the string list :
#Injectable()
export class MyService {
private myModel : Array<String> = new Array();
public get MyModel () : Array<String> {
return this.myModel;
}
/// Assume you have method in the service to populate the model...
}
Then it's your component which generate the HCI :
export class AppComponent {
/// Use dependency injection to get service :
constructor (private _myService : MyService){}
public get MyModel () : Array<String> {
return this.myService.MyModel;
}
}
Finally in your component template :
<div>
<ul>
<li *ngFor="let s of MyModel">
<!-- Button with your model text -->
<button>s</button>
</li>
</ul>
</div>
That is a better solution than generate the components in the service because just imagine you don't want buttons list but a togglebuttons list in your HCI, here you just have to change the HTML. The service is still the same, and the components typescipt part is still the same too !