Elm with webcomponents - Is this a bug or am I doing something wrong? - web-component

Problem summary
I am using webcomponents in elm 0.19.1.
Goal is for the webcomponent to emit an event if the webcomponent attribute "requeststate" changes.
The event is emitted (I can see in the the logging in the webcomponent constructor), but elm is not firing the 'WebcomponentEvent' correctly.
Problem is on both windows 10 and ubuntu 16.04, firefox and chrome.
Didnot test older elm versions.
Steps to reproduce:
I have the problem reproduced in this minimal repo:
https://github.com/Derryrover/elm_webcom_bug
Compile elm with:
elm make src/Main.elm --output elm.js --debug
use a webserver (for example 'serve') to host locally
click the button 'request'. This will change the attribute 'requeststate' on the webcomponent. That in turn triggers the custom event 'created' on the webcomponent. But elm does not receive this event..
Opening the elm-debugger or clicking the 'request' button again will magically make the event fire in elm. Strange.
Also I have made the branch 'tom-experiment'. In this branch I got the event from webcomponent-click working, but only if I directly click on the webcomponent itself.
The importance of this problem
Why do I want to trigger an event on a webcomponent by changing an attribute?
Goal of this approach is also JavaScript interop.
For example I can now use this webcomponent to create a date-time or uuid or do some other javascript magic. This way I can work around ports completely.
A solution for this problem might settle the entire Javascript interop discussion !
Code
This is my Main.elm:
module Main exposing (..)
import Browser
import Html
import Html.Attributes
import Html.Events
import Json.Decode
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
type alias Model =
{ request : Bool
, received: Bool
}
init : Model
init = { request = False
, received = False
}
type Msg
= Request
| Reset
| WebcomponentEvent
update : Msg -> Model -> Model
update msg model =
case msg of
WebcomponentEvent ->
{ model | received = True}
Request ->
{ model | request = True }
Reset ->
{ model | request = False , received = False}
view : Model -> Html.Html Msg
view model =
Html.div []
[ Html.button [Html.Events.onClick Request] [Html.text "Request"]
, Html.div
[]
[ Html.text "requested:"
, Html.text (if model.request then "true" else "false")
]
, Html.div
[]
[ Html.text "received:"
, Html.text (if model.received then "true" else "false")
]
, Html.button [Html.Events.onClick Reset] [Html.text "Reset"]
, Html.node "webcomponent-test"
[ Html.Events.on "created" (Json.Decode.succeed WebcomponentEvent)
, Html.Attributes.attribute "requeststate" (if model.request == True then "requested" else "idle")
] []
]
This is the webcomponent.js
class Webcomponent extends HTMLElement {
static get observedAttributes() {
return [
"requeststate"
];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "requeststate":
if (newValue === "requested") {
console.log(`requested in webcomponent triggered`);
const customEvent = new CustomEvent('created', {detail: ""});
this.dispatchEvent(customEvent);
}
}
}
constructor() {
super();
this.addEventListener('created', function (e) {
console.log("event triggered as sensed by javascript: ", e.detail, e);
}, false, true);
}
}
customElements.define('webcomponent-test', Webcomponent);
this is my index.html
<!doctype html>
<html>
<head>
<script type="module" src="./webcomponent.js"></script>
<script src="./elm.js"></script>
</head>
<body>
<div id="elm_element"></div>
<script>
var app = Elm.Main.init({
node: document.getElementById('elm_element')
});
</script>
</body>
</html>

This was discussed on the Elm Slack. The issue ended up being a timing issue. The resolution is to change from
this.dispatchEvent(customEvent)
to
requestAnimationFrame(() => this.dispatchEvent(customEvent))
in the custom element, as can be seen in this ellie https://ellie-app.com/cqGkT6xgwqKa1.
Thanks to #antew for the final solution.

There is another potential cause:
This attributeChangedCallback runs before the element is connected to the DOM
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "requeststate":
if (newValue === "requested") {
console.log(`requested in webcomponent triggered`);
const customEvent = new CustomEvent('created', {detail: ""});
this.dispatchEvent(customEvent);
}
}
}
Add
connectedCallback(){
console.log("Now I am ready to emit Events");
}
to verify your dispatchEvent doesn't run too soon.
requestAnimationFrame (or setTimeout) are workarounds to wait till the Event Loop is empty (and thus connectedCallback ran)
(I don't know your use case) You could also test for oldValue === null or this.isConnected in your attributeChangedCallback
Also note you probably need bubbles:true and composed:true on that CustomEvent when shadowDOM is involved.

Related

Listening for change to property in ngrx

In my first component I have set up a property like so:
linkEnabled: boolean = false;
and when this is set to false, certain routes will not be able to be accessed by users which I've set up in my html file like so:
<a class="nav-link" [routerLink]="linkEnabled ? ['/libraries']: null" [routerLinkActive]="linkEnabled ? 'active-route' : 'is-disabled'">
This is set to false until a project has been selected, this is done in another component
In the second component I've imported the first one like so:
import { NavSidebarComponent } from '../nav-sidebar/nav-sidebar.component';
and added it to the constructor:
constructor(private store: Store<AppState>,
..........
private navSidebarComponent: NavSidebarComponent
) { }
and in the ngOnit, where the project is set, I call the linkEnabled value and set to true for when project name is not null:
this.projectNameSub = this.store.pipe(select(ProjectSelectors.selectCurrentProjectName))
.subscribe(projectName => {
this.projectName = projectName;
if(this.projectName !=null) {
this.navSidebarComponent.linkEnabled = true;
}
});
The issue I am having, is that I am not sure how to get the first component to listen to the changes so that it know that linkEnabled has now been set to true? As at the moment it just sees it as false so I know I am missing a step but I'm just not sure what. Is there a way to subscribe to the value so that it can listen to it changing in the ngOnInit in the first component?
I had thought of creating a function like so within the first component:
public activateRoutes(): Observable<boolean> { console.log("activate routes called"); return of(this.linkEnabled = true); }
and then in the ngOnit do something like:
this.activateRoutes().subscribe((link) => { this.linkEnabled = link; })
and then in the ngOnit in the second component, instead of doing:
this.navSidebarComponent.linkEnabled = true;
I would do: this.navSidebarComponent.activateRoutes();
However all that is happening is that on page load, the linkEnabled is set to true and it's not working at all as I need it to
Solved this issue by creating new action, reducer and selectors file to store this in the store and could then do:
this.routeEnabledSub = this.store.pipe(select(RouteSelectors.selectRouteEnabled))
.subscribe(routeEnabled => {
this.routeEnabled = routeEnabled;
});
in the first component
and then in the second one, update it like so:
if(this.projectName !=null) {
this.store.dispatch(RouteActions.setRouteActive(
{ routeActive: true }));
} else {
this.store.dispatch(RouteActions.setRouteActive(
{ routeActive: false }));
and in html check for linkEnabled being set like so:
[routerLinkActive]="linkEnabled ? 'active-route' : 'is-disabled'"

stenciljs: Issue with slot

I have created a simple web (stencil) component AuthGuard, not to be be confused with Angular's AuthGuard.
The purpose of this component is to check if the user is logged in.
If yes, render the slot html.
If not, render the Signup button.
The component code is as follows:
import { Component, Host, h } from '#stencil/core';
import { Build, State } from '#stencil/core';
import { AuthService } from 'auth/auth.service';
import { ConfigService } from 'common/config.service';
#Component({
tag : 'auth-guard',
styleUrl : 'auth-guard.css',
shadow : true,
})
export class AuthGuard {
#State() canRender : boolean = false;
componentWillLoad() {
if (Build.isBrowser) {
const timerId = setInterval(() => {
if (AuthService.isInitialized) {
AuthService.vol$.subscribe(_u => {
this.canRender= true;
});
clearInterval(timerId);
}
}, ConfigService.loadTime);
}
}
render() {
console.log('auth guard :: render', this.canRender, AuthService.me);
return (
<Host>
{
this.canRender ? (
AuthService.me && AuthService.me.id.length > 0 ? (
<slot></slot>
) : (
<ion-button
href="/signup"
routerDirection="forward"
color="danger">
Signup
</ion-button>
)
): null
}
</Host>
);
}
}
Now in the other file, I use the following code:
<auth-guard slot='end'>
<volunteer-mini volunteer={AuthService.me}></volunteer-mini>
</auth-guard>
With this what I am expecting is
Nothing to be rendered, till this.canRender becomes true.
Once this.canRender becomes true, If AuthService.me is valid, render the slot HTML,
If AuthService.me is null, render signup button.
But seems when this.canRender is false, it tried to render volunteer-mini the slot HTML, which is a problem. Since volunteer-mini internally depends on AuthService.me, which is not yet initialized.
But once this.canRender becomes true, other 2 scenarios are working fine.
It's in general a bad idea to write an auth-guard using stencil. The core problem is that your slot exists before your component has initialized.
Therefor, using your current code, you'd have to manually remove the slot after you decided that you don't have the rights.
Additionally, if you do not define a slot position, but still provide a slot-content in your parent, it will still be appended to your inner children.
To resolve this problem, you can refactor your component to an function, like <Host>, but this has other pits to consider.

Integrate MPGS session.js in Aurelia SPA

I am trying to integrate the MasterCard Payment Gateway services to a SPA using Aurelia.
I am trying to use the hosted checkout mechanism to integrate MPGS.
I am writing code in the attached event of the Aurelia viewmodel to load the session.js library and on load of the library adding another script element to initialize the session.
The attached method is listed below.
this.mpgsSessionScript = `if (self === top) { var antiClickjack = document.getElementById("antiClickjack"); antiClickjack.parentNode.removeChild(antiClickjack); } else { top.location = self.location; } PaymentSession.configure({ fields: { card: { number: "#card-number", securityCode: "#card-cvv", expiryMonth: "#card-expiry-month", expiryYear: "#card-expiry-year" } }, frameEmbeddingMitigation: ["javascript"], callbacks: { initialized: function(response) { console.log(response); }, formSessionUpdate: function(response) { } } }); function pay() { PaymentSession.updateSessionFromForm('card'); }`;
this.styleElement = document.createElement('style');
this.styleElement.setAttribute('id', "antiClickjack");
this.styleElement.innerText = "body{display:none !important;}";
document.head.appendChild(this.styleElement);
this.scriptElement = document.createElement('script');
this.scriptElement.src = this.sessionJs;
this.scriptElement.onload = () => {
const innerScriptElement = document.createElement('script');
innerScriptElement.innerText = this.mpgsSessionScript;
document.body.appendChild(innerScriptElement);
};
document.body.appendChild(this.scriptElement);
I can successfully run the code the and the script elements are added to DOM and also, the antiClickjack style gets removed, which indicates that the script element gets added.
But the issue is, the card number and the cvv elements are not getting enabled for input. If successfully initialized, the card number and the cvv elements should get enabled.
I do not see any errors in the console. Any help would be appreciated.

Letting a user use a compiled RequireJS Widget after it has loaded

I'm writing a JS Widget using RequireJS. After finishing the widget I'm compiling it with r.js and Almond. All goes well - but I couldn't find an easy way to let the user use the widget without using RequireJS himself - as the widget code loads async (RequireJS uses AMD).
What I'm doing now is busy waiting for the widget code to load and using it only after detecting it has loaded. This is not very user-friendly.
Is there a way to let just do something like this?
var widget = new Widget();
instead of doing busy wait like:
count = 0;
function loadWidget() {
if (typeof Widget != 'undefined') {
var p1 = new Widget();
p1.render();
} else {
if (count > 10) {
console.log('Failed to load the Widget');
return false;
}
setTimeout(loadWidget, 50);
count++;
}
}
$(document).ready(function() {
loadWidget();
});
Thanks!
EDIT:
my build.js
({
name: './lib/almond.js',
out: './deploy/sdk.min.js',
baseUrl: '.',
optimize: 'uglify2',
mainConfigFile: 'sdk.js',
include: ['sdk'],
wrap: true
})
Code on the web page (assume no other script tags on page):
<script src="mywidget.js" data-main="scripts/sdk" id="mywidget"></script>
No sure if the 'data-main' is really required as the js is compiled.
You need to follow the instructions provided with Almond. To summarize the essential points what is in the doc there, what you need in your build config the following configuration:
wrap: {
startFile: 'path/to/start.frag',
endFile: 'path/to/end.frag'
}
And the start.frag should be:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else {
root.Widget = factory();
}
}(this, function () {
and the end.frag:
return require('main');
}));
The end fragment calls require in its synchronous form, which is really synchronous in this case (and not the pseudo-synchronous sugar that can be used by RequrieJS itself).
I've tried this before. It works.

How to attach element to another Ajax component

All, Forgive me I am not familiar with the ASP.NET Ajax. I knew the method Create is attaching an html element to ajax component. But I don't know how to detach it from the current component . and attach another one.
Let's say there is a element ctl00_PlaceHolderMain_UserRegistration_txbPassword1 has been attached to a component type AccelaWebControlExtender.HelperBehavior, and the created component id is ctl00_PlaceHolderMain_UserRegistration_txbPassword1_helper_bhv. The code looks like below. please review it .
Sys.Application.add_init(function() {
$create(AccelaWebControlExtender.HelperBehavior, {"closeTitle":"Close","id":"ctl00_PlaceHolderMain_UserRegistration_txbPassword1_helper_bhv","isRTL":false,"title":"Help"}, null, null, $get("ctl00_PlaceHolderMain_UserRegistration_txbPassword1"));
});
I think firstly I should retrieve the component by id, then do the detach and attach work. Hope someone can give me some help.Thanks.
After doing some research, I found It is called Extend Web server control that encapsulates a client behavior in Asp.net Ajax, And I found the attachment of component is done by Asp.net automatically . We can see the Sys.Application.add_init(function() code is generated in the aspx page by Asp.net automatically. So if we want to customize the original behavior of Web Server Control, I believe it can be made in the Javascript OOP way(old and same).
For example :
If the original behavior code is blow.
// Register the namespace for the control.
Type.registerNamespace('Samples');
//
// Define the behavior properties.
//
Samples.FocusBehavior = function(element) {
Samples.FocusBehavior.initializeBase(this, [element]);
this._highlightCssClass = null;
this._nohighlightCssClass = null;
}
//
// Create the prototype for the behavior.
//
Samples.FocusBehavior.prototype = {
initialize : function() {
Samples.FocusBehavior.callBaseMethod(this, 'initialize');
$addHandlers(this.get_element(),
{ 'focus' : this._onFocus,
'blur' : this._onBlur },
this);
this.get_element().className = this._nohighlightCssClass;
},
dispose : function() {
$clearHandlers(this.get_element());
Samples.FocusBehavior.callBaseMethod(this, 'dispose');
},
//
// Event delegates
//
_onFocus : function(e) {
if (this.get_element() && !this.get_element().disabled) {
this.get_element().className = this._highlightCssClass;
}
},
_onBlur : function(e) {
if (this.get_element() && !this.get_element().disabled) {
this.get_element().className = this._nohighlightCssClass;
}
},
//
// Behavior properties
//
get_highlightCssClass : function() {
return this._highlightCssClass;
},
set_highlightCssClass : function(value) {
if (this._highlightCssClass !== value) {
this._highlightCssClass = value;
this.raisePropertyChanged('highlightCssClass');
}
},
get_nohighlightCssClass : function() {
return this._nohighlightCssClass;
},
set_nohighlightCssClass : function(value) {
if (this._nohighlightCssClass !== value) {
this._nohighlightCssClass = value;
this.raisePropertyChanged('nohighlightCssClass');
}
}
}
// Optional descriptor for JSON serialization.
Samples.FocusBehavior.descriptor = {
properties: [ {name: 'highlightCssClass', type: String},
{name: 'nohighlightCssClass', type: String} ]
}
// Register the class as a type that inherits from Sys.UI.Control.
Samples.FocusBehavior.registerClass('Samples.FocusBehavior', Sys.UI.Behavior);
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
I think we can override some of the methods of the Javascript Object Samples.FocusBehavior and it's prototype object to achieve customization.
For example .
I can override Samples.FocusBehavior.prototype._onFocus in the script like this.
Samples.FocusBehavior.prototype._onFocus = function (e) {
alert('test');
if (this.get_element() && !this.get_element().disabled) {
this.get_element().className = this._highlightCssClass;
}
};
Just make sure this code is parsed after original one by Browser.
I am not sure if this is the right way to make it . I hope someone can help to verify it .Thank you very much.
Here is a tutorial of it. please review it .
Cheers.

Resources