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
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 am converting a custom element dropdown over to lit-element. The way the existing element shows the dropdown options is by setting an expanded boolean attribute on the element, and the options are shown/hidden via css:
my-element:not([expanded]) .options-container {
display: none;
}
my-element[expanded] .options-container {
display: block;
}
The component doesn't need to do any rerenders because the logic is all in the css.
How can I achieve this behavior with lit-element, and not rerender the component? Rerendering can be costly if there are a lot of dropdown options.
I have tried implementing a shouldUpdate that returns false if only expanded has changed - but this causes lit-element not to reflect expanded to the attribute when set via a property, which is necessary in order to show/hide via css.
This is what I have, which doesn't work:
class MyDropdown extends LitElement {
static get properties() {
return {
expanded: { type: Boolean, reflect: true },
...
};
}
shouldUpdate(changedProperties) {
if (changedProperties.has('expanded') && changedProperties.size === 1) {
return false;
}
return true;
}
// disable shadow-dom
createRenderRoot() {
return this;
}
}
Note that I am not using shadow dom yet, not sure if that would change the solution. I'm on lit-element 2.2.1.
The idea is to not use LitElement's static properties or #Property decorator. Write your own property implementation like this:
class MyDropdown extends LitElement {
_expanded = false;
get expanded() {
return this._expanded;
}
set expanded(val) {
this._expanded = val;
// Manually setting the property and reflecting attribute.
if (val) {
this.setAttribute('expanded', '');
} else {
this.removeAttribute('expanded');
}
}
// disable shadow-dom
createRenderRoot() {
return this;
}
}
Similarly, you can listen for attributeChangedCallback lifecycle event and adjust _expanded property whenever user changes the attribute and not property.
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
There are actually two challenges I face. I am looping through an array of values and need to
set a class name depending on an observable variable from a child
component.
reevaluate the class as soon as the child variable changes.
location.component.ts
import { Component, Input } from '#angular/core';
import { BusinessLocation, SpecialDays, RegularHours } from './location';
import { DbService } from './db-service.component';
#Component({
selector: '[data-locations]',
templateUrl: 'app/location.component.html',
providers: [DbService]
})
export class LocationComponent {
locations:BusinessLocation[];
selectedLocationId:Number;
constructor(private api:DbService){}
isOpenOnDay(day):Boolean {
let _weekDay = day.getDay();
let _retour = false;
this.locations.forEach(loc => {
if ( loc.id == this.selectedLocationId && loc.regularHours.weekDay == _weekDay ) {
_retour = true;
}
});
this.locations.forEach(loc => {
if ( loc.id == this.selectedLocationId && loc.specialDays.singleDate.getDay() == _weekDay) {
_retour = true;
}
});
return _retour;
}
getLocation():Number {
return this.selectedLocationId;
}
setLocation(id):void {
this.selectedLocationId = id;
}
getLocations():void {
this.api.getLocations().subscribe(
locations => {
this.locations = locations as BusinessLocation[];
this.setLocation(this.locations[0].id);
}
);
}
}
a snippet from db-services.component.ts
getLocations():Observable<BusinessLocation[]> {
return this.http.get(this.apiUrl + '/get_locations.php')
.map(response => response.json().data as BusinessLocation[]);
}
}
it all works fine. However, here is where the challenge is. The parent component initiates the locations, but it also needs to know what location is selected right now. Here is the month.component.html
<span class="location-container" #location data-locations><span class="loading">Loading locations...</span></span>
<div *ngFor="let day of week.days" class="day" data-can-drop="day"
[class.today]="isToday(day)"
[class.in-other-month]="day.getMonth() != jsMonth"
[class.is-closed]="!isOpenAtLocation(day)">
<div class="day-marker"></div>
<span class="day-date">{{day | date:'d'}}</span>
<span *ngIf="checkMonth(day)" class="day-month">{{months[day.getMonth()]}}</span>
</div>
and a snippet from month.component.ts is
#ViewChild('location') locationComponent:LocationComponent;
isOpenAtLocation(day):Boolean {
return this.locationComponent.isOpenOnDay(day);
}
ngOnInit(): void {
this.locationComponent.getLocations();
}
The error I get is pretty straightforward and totally understandable:
Subscriber.ts:238 TypeError: Cannot read property 'forEach' of undefined
at LocationComponent.isOpenOnDay (location.component.ts:25)
at MonthComponent.isOpenAtLocation (month.component.ts:176)
And this is just about Challenge 1. The Challenge 2 has not been even addressed yet.
I just can't wrap my head around it. >_<
Thanks.
Well, this was a bad joke on my side. First, this is a reminder, that a change of a property of an object will reflect itself in DOM if there is a binding available. So going with [class.isOpenOnDay]="day.isOpenAtLocation" would be totally sufficient, where day is an object and isOpenAtLocation is its property. Even if it is not set initially (meaning it is null) and will be updated in the future – it is all good. This is basically how NG works (and has worked all the time). Silly me.
The other problem – changing the value depending on a child component variable – has been solved by emitting events (from child), listening to the events (in parent) and resetting the property isOpenAtLocation again.
So the updated child component location.component.ts has been updated like this:
#Output() locationChanged = new EventEmitter<Number>();
setLocation(id):void {
this.selectedLocationId = id;
this.locationChanged.emit(this.selectedLocationId);
}
The view for the location component now has this line:
<select (change)="setLocation($event.target.value)">
<option *ngFor="let loc of locations" value="{{loc.id}}">{{loc.longName}}</option>
</select>
The parent component's view is bound to the event like this:
<span class="location-container" #location data-locations (locationChanged)="onLocationChange($event)"><span class="loading">Loading locations...</span></span>
And the parent month.component.ts itself has two more methods:
onLocationChange(event) {
if ( this.selectedLocationId != event ) {
this.selectedLocationId = event;
this.setLocation();
this.dispatchResize();
}
}
setLocation():void {
if ( this.selectedLocationId >= 0) {
for ( let i = 0; i < this.weeks.length; i++) {
let _week = this.weeks[i];
_week.forEach(_day => {
let _isOpen = this.locationComponent.isOpenOnDay(_day.date);
_day['isOpenOnDay'] = _isOpen.isOpenOnDay;
_day['isSpecialDay'] = _isOpen.isSpecialDay;
_day['dayHours'] = _isOpen.dayHours;
});
}
}
}
As one can see, I have added even more dynamically checked properties, not just isOpenOnDay, but also isSpecialDay and dayHours, which are not yet defined initially, but are set as soon as the data is available – and are reflected in view as soon as they change.
Basic stuff, actually. Still might be helpful to some NG2 noob like me.
I got two classes, one deriving from the other, and on the base class I need to check if the derived class is implementing a method with a specific name:
class Foo {
constructor() { }
childHasMethod() {
if(this.method) {
console.log('Yay');
} else {
console.log('Nay');
}
}
}
class Bar extends Foo {
constructor() {
super();
this.childHasMethod();
}
method() {
}
}
var bar = new Bar();
Even though the line if(this.method) { is marked red on the playground, it works. But the local compiler throws a compilation error: The property 'method' does not exist on value of type 'Foo'.
Is there a clean way to achieve what I'm trying to do?
In order to "sneak it past the compiler" you can treat this as dynamic:
(<any>this).method
I have made a full example of this on the TypeScript Playground.
childHasMethod() {
if((<any>this).method) {
alert('Yay');
} else {
alert('Nay');
}
}
Having said this, having a base class know details about its sub-classes could get you into tricky places. Usually I would try to avoid this as it sounds like the specialisations are leaking into the base class - but you may have a particular thing you are doing and know your program better than me so I'm not saying "don't do this" - just "are you sure" :)