I have the following component:
#Component({
template: `
<div class="container">
<div *ngFor="let connection of connections">
<div class="row">
<div class='col-2'>{{connection.arrivalTime}}</div>
<div class='col-1'>{{connection.delay}}</div>
<div class='col-2'>{{connection.actualArrivalTime}}</div>
<div class='col-1'>{{connection.icon}}</div>
<div class='col-1'><span [ngStyle]="{'background-color': connection.colors.bg}">{{connection.line}}</span></div>
<div class='col-3'>{{connection.direction}}</div>
<div class='col-2'>{{connection.cancelled}}</div>
</div>
</div>
</div>
styleUrls: ['../app.component.css', '../../simple-grid.css'],
})
export class ZVVComponent {
connections: PublicConnection[] = [];
displayDate: Date;
constructor(private zvvService: ZVVService) {
this.displayDate = new Date();
zvvService.getConnections(this.displayDate).subscribe(data => {
data.forEach( (connection) => {
this.connections.push(new PublicConnection(
connection.product.line,
connection.product.longName,
connection.product.direction,
connection.cancelled,
connection.product.icon,
connection.product.color,
connection.mainLocation.time,
connection.mainLocation.countdown,
connection.mainLocation.realTime.time,
connection.mainLocation.realTime.countdown,
connection.mainLocation.realTime.delay,
connection.mainLocation.realTime.isDelayed,
connection.mainLocation.realTime.hasRealTime
));
});
});
}
}
As you can see, I used ngStyle in one of the divs and want to bind it to the variable connection.colors.bg that contains a hex string of the color:
export class Color {
get fg(): string {
return this.fg;
}
get bg(): string {
return this.bg;
}
}
However, this doesn't work and the text remains black and the background white. What am I doing wrong? When I change it, and write red in it instead of the variable, the text shows up in red.
Here is the PublicConnection code:
import { Color } from './color';
export class PublicConnection {
constructor(
public line: string,
private name: string,
public direction: string,
public cancelled: boolean,
public icon: string,
public colors: Color,
public arrivalTime: string,
private countdown: string,
public actualArrivalTime: string,
private actualCountdown: string,
public delay: string,
private isDelayed: boolean,
private hasRealtimeData: boolean
) {
this.direction = this.direction.replace('ü', 'ü');
this.direction = this.direction.replace('ö', 'ö');
this.direction = this.direction.replace('ü', 'ü');
}
}
The issue is not with the ngStyle directive -- you are using that correctly. It is most likely the data not being loaded when the component first tries to render.
Since your data is asynchronous, I'm guessing that at the time the component is rendering and setting the background color, it has not yet received a color from the service.
Try using a safe navigation operator by changing connection.color.bg to connection.color?.bg in your template.
Read more about it here: https://angular.io/guide/template-syntax#the-safe-navigation-operator----and-null-property-paths
Related
I am following the Microsoft tutorial for creating an Application Customizer. That works great, but now I would like to add some custom things like a curtain menu to my site.
I can't seem to figure out how to add an event listener to an element that is being rendered from a promise. The element doesn't exist initially, so if I try the ID, I get a null error. If I use a class it gives me an inline script error.
I've searched for a solution, but don't see one that applies to what I am doing.
I know this code is a mess, but I've been trying so many different methods, I can't seem to find one that works.
import { override } from '#microsoft/decorators';
import { Log } from '#microsoft/sp-core-library';
import {
BaseApplicationCustomizer,
PlaceholderContent,
PlaceholderName,
PlaceholderProvider
} from '#microsoft/sp-application-base';
import { Dialog } from '#microsoft/sp-dialog';
import * as strings from 'HideSideNavApplicationCustomizerStrings';
// import * as strings from './myStrings';
import styles from './AppCustomizer.module.scss';
import {escape} from '#microsoft/sp-lodash-subset';
import Placeholder from '#microsoft/sp-application-base/lib/extensibility/placeholder/Placeholder';
const LOG_SOURCE: string = 'HideSideNavApplicationCustomizer';
let openBtn = "openBtn";
export interface IHideSideNavApplicationCustomizerProperties {
// This is an example; replace with your own property
testMessage: string;
Top: string;
openBtn: string;
}
/** A Custom Action which can be run during execution of a Client Side Application */
export default class HideSideNavApplicationCustomizer
extends BaseApplicationCustomizer<IHideSideNavApplicationCustomizerProperties> {
private _topPlaceholder: PlaceholderContent | undefined;
#override
public onInit(): Promise<void> {
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
return Promise.resolve();
}
private _renderPlaceHolders(): void {
console.log('calling _renderPlaceHolders');
console.log(
"Available placeholders: ",
this.context.placeholderProvider.placeholderNames
.map(name => PlaceholderName[name])
.join(", ")
);
if(!this._topPlaceholder){
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Top,
{ onDispose: this.onDispose}
)
}
if(!this._topPlaceholder) {
console.error("The expected placeholder (Top) was not found.");
return;
}
if(this.properties){
let topString: string = `
<!-- The overlay -->
<div id="myNav" class="navClose overlay">
<!-- Button to close the overlay navigation -->
×
<!-- Overlay content -->
<div class="overlay-content">
About
Services
Clients
Contact
</div>
</div>
<!-- Use any element to open/show the overlay navigation menu -->
<span class="navOpen">open</span>
`;
if(!topString){
topString = "(Top property was not defined.)";
}
if(this._topPlaceholder.domElement){
this._topPlaceholder.domElement.innerHTML=topString;
let navState :string = "closed";
const navClose = document.getElementsByClassName("navClose").item(0);
this._topPlaceholder.domElement.addEventListener("click", function(e){
if(navState == "closed"){
navClose.setAttribute(this.style.width, "100%");
navState = "opened";
}
else{
navClose.setAttribute(this.style.width, "0");
navState = "closed";
}
});
}
}
}
private _onDispose(): void {
console.log('[HideSideNavApplicationCustomizer._onDispose] Dispose custom top and bottom.')
}
}
So I think I finally figured this out. I wasn't using 'this' correctly after the promise was made, and there were a number of syntax errors with how I was trying to update the attributes on the elements.
Here's my updated code in case anyone is trying to do something similar.
import { override } from '#microsoft/decorators';
import { Log } from '#microsoft/sp-core-library';
import {
BaseApplicationCustomizer,
PlaceholderContent,
PlaceholderName,
PlaceholderProvider
} from '#microsoft/sp-application-base';
import styles from './AppCustomizer.module.scss';
const LOG_SOURCE: string = 'HideSideNavApplicationCustomizer';
let openBtn = "openBtn";
export interface IHideSideNavApplicationCustomizerProperties {
// This is an example; replace with your own property
testMessage: string;
Top: string;
}
/** A Custom Action which can be run during execution of a Client Side Application */
export default class HideSideNavApplicationCustomizer
extends BaseApplicationCustomizer<IHideSideNavApplicationCustomizerProperties> {
private _topPlaceholder: PlaceholderContent | undefined;
#override
public onInit(): Promise<void> {
this.context.placeholderProvider.changedEvent.add(this, this._renderPlaceHolders);
return Promise.resolve();
}
private _renderPlaceHolders(): void {
console.log('calling _renderPlaceHolders');
console.log(
"Available placeholders: ",
this.context.placeholderProvider.placeholderNames
.map(name => PlaceholderName[name])
.join(", ")
);
if(!this._topPlaceholder){
this._topPlaceholder = this.context.placeholderProvider.tryCreateContent(
PlaceholderName.Top,
{ onDispose: this.onDispose}
)
}
if(!this._topPlaceholder) {
console.error("The expected placeholder (Top) was not found.");
return;
}
if(this.properties){
let topString: string = `
<!-- The overlay -->
<div id="nav" class="${styles.overlay}">
<!-- Button to close the overlay navigation -->
×
<!-- Overlay content -->
<div class="${styles['overlay-content']}">
About
Services
Clients
Contact
</div>
</div>
<!-- Use any element to open/show the overlay navigation menu -->
<span id="navOpen">open</span>
`;
if(!topString){
topString = "(Top property was not defined.)";
}
if(this._topPlaceholder.domElement){
const top = this._topPlaceholder.domElement;
top.innerHTML=topString;
let nav = top.querySelector('#nav');
let navOpen = top.querySelector('#navOpen');
let navClose = top.querySelector('#navClose');
navOpen.addEventListener("click", () => {
nav.setAttribute("style","width:75%;");
});
navClose.addEventListener("click", () => {
nav.setAttribute("style","width:0%;");
});
}
}
}
private _onDispose(): void {
console.log('[HideSideNavApplicationCustomizer._onDispose] Dispose custom top and bottom.')
}
}
I'm stucking on one special thing where I try to pass an object of RenderFragment to a dynamically generated component.
I considered this sample from Devexpress https://docs.devexpress.com/Blazor/401753/common-concepts/customize-and-reuse-components
<DxFormLayout>
<DxFormLayoutTabPages>
#renderLayoutTabPage()
</DxFormLayoutTabPages>
</DxFormLayout>
#code {
private RenderFragment renderLayoutTabPage() {
RenderFragment item = b => {
b.OpenComponent<DxFormLayoutTabPage>(0);
b.AddAttribute(1, "Caption", "My tab");
b.AddAttribute(2, "ChildContent", (RenderFragment)((tabPageBuilder) => {
tabPageBuilder.OpenComponent<DxFormLayoutItem>(0);
tabPageBuilder.AddAttribute(1, "Caption", "DynLayoutItem");
tabPageBuilder.AddAttribute(2, "ColSpanMd", 6);
tabPageBuilder.AddAttribute(5, "Template", (RenderFragment<Object>)((context) => ((itemTemplate) => {
itemTemplate.OpenComponent<DxTextBox>(0);
itemTemplate.AddAttribute(1, "Text", text);
itemTemplate.CloseComponent();
})));
tabPageBuilder.CloseComponent();
}));
b.CloseComponent();
};
return item;
}
}
So this is the way they are building a complete new ChildContent Fragment.
my razor file looks like:
[Parameter] RenderFragment<object> DisplayTemplate {get;set;} //pass this Fragement to the dynamic component
protected override void OnInitialized()
{
...
b.AddAttribute(3, nameof(DxDataGridColumn.DisplayTemplate), (RenderFragment<Object>)((context) => ((itemTemplate) =>
{
itemTemplate.AddContent<object>(0, DisplayTemplate, context);
})));
...
}
When I run this, the DisplayTemplate does not get rendered. I only can see the type string "Microsoft.AspNetCore.Components.RenderFragment`1[System.Object]"
What I'm doing wrong here?
Got it!
In my Component I was using this pattern
<MyComponent>
<DisplayTemplate>
#DisplayTemplate
</DisplayTemplate>
<MyComponent>
The only way it works correct is to use the Attributes directly:
<MyComponent DisplayTemplate"#DisplayTemplate" />
The code is pretty straightforward: I'm try to pass the function addContactFn() from MainComp to SideMenu. On click I get the error
Uncaught TypeError: this.value.handleEvent is not a function
class MainComp extends LitElement {
constructor(){
super()
this.addContactFn = this.addContactFn.bind(this)
}
addContactFn() {
console.log("clicked");
}
render(){
return html`
<div class="main-page">
<side-menu addContactFn="${this.addContactFn}"></side-menu>
</div>
`
}
}
class SideMenu extends LitElement {
constructor(){
super()
}
static get properties(){
return {
addContactFn: Function
}
}
render(){
return html`<a #click="${this.addContactFn}">Add contact</a>`
}
}
As Thad said, attributes are always strings, and there's no real safe efficient way of parsing a function in execution
However, you don't really even need to use that, just pass the function as a property rather than as an attribute and that should be enough, here's how MainComp's render would end up after that
render(){
return html`
<div class="main-page">
<side-menu .addContactFn="${this.addContactFn}"></side-menu>
</div>
`;
}
Basically, you just add a dot before the property name
For more info check LitElement's guide
Then again, this way of doing stuff is very React-ish and not really recommended for Web Components, you should probably just create a emit a custom event in the child component and pick it up in the parent
I downloaded Skeleton-starter-kit application and started creating my new web page project in Polymer. The application has 3 different pages. On the first page I used iron-ajax and app-storage to connect to the API and store its values to the local storage. Now I would like to use data stored in that local storage on the other two pages of the project but I dont know how. I tried with this.$.propertyname but I only got undefined. I would appreciate any kind of help.
First page:
<dom-module id="first-page">
<iron-ajax
auto
url="http://api.blog.diamondappgroup.com/blog/posts"
handle-as="json"
last-response="{{ajaxResponse}}">
</iron-ajax>
<app-indexeddb-mirror
key="posts"
data="{{ajaxResponse.data}}"
persisted-data="{{persistedData}}">
</app-indexeddb-mirror>
<template is="dom-repeat" items={{persistedData}} as="data">
<div id="{{index}}" class="imageTextContainer1" on-click="showPost">
<img id="{{index}}" src$="{{openedBlogImage}}" alt="Image Not Found" on-click="showPost"/>
<h3 id="{{index}}" class="h3heading" on-click="showPost"> {{openedBlogName}} </h3>
<p id="{{index}}" class="text" on-click="showPost">{{openedBlogBody}}</p>
</div>
</template>
<script>
class FirstPage Polymer.Element {
static get is() { return "first-page"; }
static get properties() { return {
persistedData: {
type: Array
},
idnumber: {
type: Number
value: 5
},
openedBlogImage: {
type: String
},
openedBlogName: {
type: String
},
openedBlogBody: {
type: String
}
}}
json(obj) {
return JSON.stringify(obj, null, 2);
}
showPost(event){
var id = parseInt(event.target.id);
this.idnumber = id;
console.log("Id: " + this.idnumber);
var image = this.persistedData[this.idnumber].images[0].path;
console.log("Image: " + image);
this.openedBlogImage = image;
var name = this.persistedData[this.idnumber].name;
console.log("Name: " + name);
this.openedBlogName = name;
var body = this.persistedData[this.idnumber].body;
console.log("Body: " + body);
this.openedBlogBody = body;
}
customElements.define(FirstPage.is, FirstPage);
Second page:
<div on-click="showPost">
<img src$="{{openedBlogImage}}" alt="Image Not Found"/>
<h3 class="h3heading"> {{openedBlogName}} </h3>
<p class="text">{{openedBlogBody}}</p>
</div>
When I click on the a link at the first page it redirect me to the second page. I would like to know how to use data stored on the first page at the second page through data binding.
I have a type script component that defines two interfaces like:
interface Project {
name: string;
activity: string;
lastBuildStatus: string;
lastBuildTime: string;
lastBuildLabel: string
webUrl: string;
}
interface GoArray {
projects: Array<Project>;
}
The components makes a call to an ASP Core controller to return an object with property 'Projects' Which itself is a list of 'Project' objects. So I believe this matches my interface definitions. In the controller I set an object of type GoArray to the result from the controller
export class GoComponent {
public projectsArray: GoArray;
constructor(private http: Http) {
}
public getPipelineStatus(chosenUsername: string, chosenPassword: string, chosenUrl: string) {
// debugger;
this.http.get('api/go/cctray?username=' + chosenUsername + '&password=' + chosenPassword + '&uri=' + chosenUrl).subscribe(result => {
this.projectsArray = result.json();
});
This seems to be returning data in the format I expect. But how do I then display all items in the array in my components HTML?
I've tried
<div *ngIf="projectsArray">
<div *ngFor='let project of projectsArray.projects'>
{{project.Name}}
{{project.Activity}}
</div>
Nothing gets rendered on the screen with this, but if I inspect the component using Augury, it appears as though the projectsArray is populated as I expected
How do I iterate over the list and display the properties of each of the objects?
Thanks
It should be,
<div *ngFor='let project of projectsArray.projects'>
{{project.name}}
{{project.activity}}
</div>