How to add an event listener to an element that is created from a promise in SPFx - spfx-extension

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.')
}
}

Related

Symfony 5 - Easyadmin 3 - How use bootstrap Modal and Toast in a stimulus controller

For my administration, i develop a new button Field to display on the index page of my userCrudController.
When clicked, it should launch an ajax action.
So to do this js logic, i create this stimulus controller.
import { Controller } from '#hotwired/stimulus';
import { Toast } from 'bootstrap';
import { createToast, createSpinner } from '../js/customs';
/*
* Ajax button Field Logic
*/
export default class extends Controller {
static targets = [];
sucessToast = null;
failedToast = null;
disable = false;
connect() {
// Search if Toast Element is already create on the page
// if not, we create it
let sucessToast = document.querySelector('#successAjaxToast');
if(!sucessToast) {
createToast('successAjaxToast', 'bg-success', 4000);
sucessToast = document.querySelector('#successAjaxToast');
}
let failedToast = document.querySelector('#failedAjaxToast');
if(!failedToast) {
createToast('failedAjaxToast', 'bg-danger', 4000);
failedToast = document.querySelector('#failedAjaxToast');
}
this.sucessToast = new Toast(sucessToast);
this.failedToast = new Toast(failedToast);
}
runAjax(event) {
let url = event.params.ajaxUrl;
let sucessToast = document.querySelector('#successAjaxToast');
let failedToast = document.querySelector('#failedAjaxToast');
let saveInnerHtml = event.target.innerHTML;
let dims = event.target.getBoundingClientRect();
if (!this.disable) {
this.disable = true;
event.target.innerHTML = createSpinner('').outerHTML;
event.target.style.width = dims.width + 'px';
fetch(url)
.then(obj => obj.json())
.then(data => {
if (data.error) {
throw data.errorMessage;
}
if (data.successMessage) {
sucessToast.querySelector('.toast-body').innerHTML = data.successMessage;
} else {
sucessToast.querySelector('.toast-body').innerHTML = 'Succès';
}
this.sucessToast.show();
})
.catch(e => {
failedToast.querySelector('.toast-body').innerHTML = e;
this.failedToast.show();
console.warn(e)
})
.finally(() => {
this.disable = false;
event.target.innerHTML = saveInnerHtml;
event.target.style.width = 'auto';
})
;
}
}
}
The problem is that I import Toast from bootstrap.
And that makes me end up with two bootstraps loaded on my page.
And so my dropdown action buttons, didn't open anymore when i click on '...' (because the action is launch twice)
I tried to override the index template in order to don't load easyadmin "app.js"
It's work,bootstrap is load only once, but of course, this solution make me lost some js logic, like "click on delete" button who don't work anymore.
So this is not a real good solution.
Somebody know how i could use bootstrap Toast in my stimulus controller without loading twice bootstrap js logic?
Thank for help :)

Passing a function to a child

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

ngStyle binding not working when using scope variable

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

Angular2: Set CSS class from Observable

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.

Flex Widgets - Inheritance of styles

I have a main application (Flex 4.6), in which I intend to use any number of widgets. The widgets are .swf files (s:Module OR s:Application, Flex 4.6).
My problem is that the loaded widget does NOT inherit the styles of the application which is using it.
To put it briefly, I load the widget as an .swf file from the sever (using URLLoader class). After downloading it, I create the instances of the widgets (whereas a single widget can be cointained in the main application on several various places - multiply).
In the main application, the following CSS file is used:
<fx:Style source="css/common.css" />
common.css content is:
s|TextInput {
contentBackgroundColor: #9FD1F2;
focusColor: #8FD7F9;
skinClass: ClassReference("skins.textInputTestSkin");
}
s|Label {
color: #2211FF;
}
And this is how I create and load the widgets:
private var bytesLoader:Loader = null;
public var loadedApp:SystemManager = null;
public var loadedModule:Module = null;
...
bytesLoader.addEventListener(Event.COMPLETE, onBytesLoaderComplete);
var context:LoaderContext = new LoaderContext(false, ApplicationDomain.currentDomain);
bytesLoader.loadBytes(urlLoader.data, context);
...
private function onBytesLoaderComplete(e:Event):void {
var dataContent:DisplayObject = bytesLoader.content;
//(Application)
if(dataContent && (dataContent is SystemManager)) {
loadedApp = dataContent as SystemManager;
loadedApp.addEventListener(FlexEvent.APPLICATION_COMPLETE,appWidgetCreationComplete);
appHolder.addChild(dataContent);
} else if(dataContent is IFlexModuleFactory) {
//(Module)
var moduleLoader:LoaderInfo = LoaderInfo(e.target);
moduleLoader.content.addEventListener("ready", moduleWidgetReadyHandler);
}
}
private function moduleWidgetReadyHandler(e:Event):void {
var factory:IFlexModuleFactory = IFlexModuleFactory(e.target);
if(factory) {
loadedModule = factory.create() as Module;
if(loadedModule) {
this.addElement(loadedModule);
}
}
}
My question is first, in what way can I apply the styles of the parents on the widget and secondly(s:Module), in what way is it possible for me to apply the styles of the parents on the widget (s:Application).
UPDATE 1
If I change getter moduleFactory (as seen below) in every single of the widgets, the styles are set just right. Meaning the textInput in the widget (Module and Application) has the same skin as in the main application.
override public function get moduleFactory():IFlexModuleFactory {
return FlexGlobals.topLevelApplication.moduleFactory;
}
It's workaround? It's good solution?
Ok, here is a solution:
Add before bytesLoader.loadBytes(urlLoader.data, context);
//init - moduleFactory
bytesLoader.contentLoaderInfo.addEventListener(Event.INIT, onContentLoaderInfoInit);
private function onContentLoaderInfoInit(e:Event):void {
if(bytesLoader && bytesLoader.contentLoaderInfo) {
bytesLoader.contentLoaderInfo.removeEventListener(Event.INIT, onContentLoaderInfoInit);
}
var loaderInfo:LoaderInfo = LoaderInfo(e.target);
loaderInfo.content.addEventListener(Request.GET_PARENT_FLEX_MODULE_FACTORY_REQUEST, onGetParentModuleFactoryRequest);
}
private function onGetParentModuleFactoryRequest(r:Request):void {
if(isGlobalStyleAllowed) {
if ("value" in r) {
r["value"] = FlexGlobals.topLevelApplication.moduleFactory;
}
}
//remove eventListener
if(bytesLoader && bytesLoader.contentLoaderInfo) {
var loaderInfo:LoaderInfo = LoaderInfo(bytesLoader.contentLoaderInfo);
loaderInfo.content.removeEventListener(Request.GET_PARENT_FLEX_MODULE_FACTORY_REQUEST, onGetParentModuleFactoryRequest);
}
}
It's works.

Resources