Custom Element Web Component Shadow DOM Vendor Scripts/Elements - recaptcha

When working with Custom Elements that leverage Shadow DOM, what is the official approach to injecting 3rd party scripts and elements such as Invisible reCAPTCHA which require scripts such:
<script src="https://www.google.com/recaptcha/api.js" async defer></script>`
for HTML elements such as a <button> to be leaded and reCAPTCHA to be rendered?
shadowRoot doesn't seem to have anything like head, is the script supposed to be added to the created template's innerHTML? Or is a <script> to be appended to the shadowRoot via appendChild in connectedCallback()? What is the official approach to working with 3rd party libraries within Shadow DOM? Loading the script on the page containing the rendered Custom Element doesn't seem to trigger a render due to Shadow DOM.
const template = document.createElement('template');
template.innerHTML = `
<form>
<button class="g-recaptcha"
data-sitekey="your_site_key"
data-callback='onSubmit'>Submit</button>
</form>
`;
class CustomElement extends HTMLElement {
constructor() {
super(); // always call super() first in the ctor.
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
...
}
disconnectedCallback() {
...
}
attributeChangedCallback(attrName, oldVal, newVal) {
...
}
}
Thank you for any guidance you can provide.

There's no offical approach because the solution depends on the 3rd party library implementation.
However, in the case of reCaptcha, the workaround is to expose the <button> in the normal DOM, and inject it in the Shadow DOM via the <slot> element.
class extends HTMLElement {
constructor() {
super()
this.attachShadow( {mode: 'open'} ).innerHTML= `
<form>
<slot></slot>
</form>`
}
connectedCallback() {
this.innerHTML = `
<button
class="g-recaptcha"
data-sitekey="your_site_key"
data-callback="onSubmit">Submit</button>`
}
})

Related

Mixing Alpine.js with 'static' serverside markup, while getting the benefits of binding, etc

I'm new to Alpine and struggling to wrap my head around how to make a scenario like this work:
Let's say I have a serverside built page, that contains some buttons, that represent newsletters, the user can sign up to.
The user might have signed up to some, and we need to indicate that as well, by adding a css-class, .i.e is-signed-up.
The initial serverside markup could be something like this:
<button id='newsletter-1' class='newsletter-signup'>Newsletter 1</button>
<div>some content here...</div>
<button id='newsletter-2' class='newsletter-signup'>Newsletter 2</button>
<div>more content here...</div>
<button id='newsletter-3' class='newsletter-signup'>Newsletter 3</button>
<div>and here...</div>
<button id='newsletter-4' class='newsletter-signup'>Newsletter 4</button>
(When all has loaded, the <button>'s should later allow the user to subscribe or unsubscribe to a newsletter directly, by clicking on one of the buttons, which should toggle the is-signed-up css-class accordingly.)
Anyway, then I fetch some json from an endpoint, that could look like this:
{"newsletters":[
{"newsletter":"newsletter-1"},
{"newsletter":"newsletter-2"},
{"newsletter":"newsletter-4"}
]}
I guess it could look something like this also:
{"newsletters":["newsletter-1", "newsletter-2", "newsletter-4"]}
Or some other structure, but the situation would be, that the user have signed up to newsletter 1, 2 and 4, but not newsletter 3, and we don't know that, until we get the JSON from the endpoint.
(But maybe the first variation is easier to map to a model, I guess...)
Anyway, I would like to do three things:
Make Alpine get the relation between the model and the dom elements with the specific newsletter id (i.e. 'newsletter-2') - even if that exact id doesn't exist in the model.
If the user has signed up to a newsletter, add the is-signed-up css-class to the corresponding <button> to show its status to the user.
Bind to each newsletter-button, so all of them – not just the ones, the user has signed up to – listens for a 'click' and update the model accordingly.
I have a notion, that I might need to 'prepare' each newsletter-button beforehand with some Alpine-attributes, like 'x-model='newsletter-2', but I'm still unsure how to bind them together when Alpine has initialising, and I have the data from the endpoint,
How do I go about something like this?
Many thanks in advance! 😊
So our basic task here is to add/remove a specific item to/from a list on a button click. Here I defined two component: the newsletter component using Alpine.data() creates the data (subs array), provides the toggling method (toggle_subscription(which)) and the checking method (is_subscribed(which)) that we can use to set the correct CSS class to a button. It also handles the data fetching in the init() method that executes automatically after the component is initialized. I have also created a save method that we can use to send the subscription list back to the backend.
The second component, subButton with Alpine.bind() is just to make the HTML code more compact and readable. (We can put each attribute from this directly to the buttons.) So on click event it calls the toggle_subscription with the current newsletter's key as the argument to add/remove it. Additionally it binds the bg-red CSS class to the button if the current newsletter is in the list. For that we use the is_subscribed method defined in our main component.
.bg-red {
background-color: Tomato;
}
<script src="https://unpkg.com/alpinejs#3.x.x/dist/cdn.min.js" defer></script>
<div x-data="newsletter">
<button x-bind="subButton('newsletter-1')">Newsletter 1</button>
<button x-bind="subButton('newsletter-2')">Newsletter 2</button>
<button x-bind="subButton('newsletter-3')">Newsletter 3</button>
<button x-bind="subButton('newsletter-4')">Newsletter 4</button>
<div>
<button #click="save">Save</button>
</div>
</div>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('newsletter', () => ({
subs: [],
init() {
// Fetch list of subscribed newsletters from backend
this.subs = ['newsletter-1', 'newsletter-2', 'newsletter-4']
},
toggle_subscription(which) {
if (this.subs.includes(which)) {
this.subs = this.subs.filter(item => item !== which)
}
else {
this.subs.push(which)
}
},
is_subscribed(which) {
return this.subs.includes(which)
},
save() {
// Send this.sub to the backend to save active state.
}
}))
Alpine.bind('subButton', (key) => ({
'#click'() {
this.toggle_subscription(key)
},
':class'() {
return this.is_subscribed(key) && 'bg-red'
}
}))
})
</script>

How to use the pageContext in SPFx?

I am trying to get a value from the current page by using the pageContext but I am getting either undefined or 404.
This is the situation:
In the Site pages library there are several news pages. Each news page has some tags attached to them. This tags lives in a custom column in the Site Pages library.
There are news that have 1 tag and other several tags. It can be the situation where two or more news share the same tag(s).
The goal is when I open a news page the tags that are attached to that news are also visible.
Until now I am using #pnp/pnpjs and the code looks like this:
var result: any = await sp.web.lists.getByTitle("Site Pages")
.items.getById(15)
.select("Tags")
.get();
return await result.Tags;
And it is giving me 404 error
I also tried this one:
this.context.pageContext.list('Site Pages').listItem['Tags'].get().then((items: any[]) => {
console.log(items);
});
But it giving me Cannot read property 'list' of undefined
Du you have an idea how can get the value of the Tags column asociated with the current news?
Here is an update
Now I am getting the right tag. The question now is how to show it in the screen?
import * as React from 'react';
import styles from './ReadTags.module.scss';
import { IReadTagsProps } from './IReadTagsProps';
import { sp } from '#pnp/pnpjs';
export default class ReadTags extends React.Component<IReadTagsProps, {}> {
constructor(props: IReadTagsProps) {
super(props);
}
private async getTags() {
var id = this.props.context.pageContext.listItem.id;
var result: any = await sp.web.lists.getByTitle("Site Pages")
.items.getById(id)
.select("Tags")
.get();
return await result.Tags;
}
public render(): React.ReactElement<IReadTagsProps> {
console.log(this.getTags());
return (
<div className={ styles.readTags }>
<div className={ styles.container }>
<div className={ styles.row }>
<div className={ styles.column }>
</div>
</div>
</div>
</div>
);
}
}
Regards
Amerco
What you'll probably want to do is store your tags in the state of your component. Then you can show these (if the value from state is not empty) during your render. I can highly recommend working through the React tutorial to understand React lifecycle and state/props.
https://reactjs.org/tutorial/tutorial.html
https://reactjs.org/docs/state-and-lifecycle.html
Something with getting your data in componentDidMount, storing it in the state by using this.setState and then running through them in render with this.state.tags. It's more of a React question then a SPFx question :)
There's a ton of samples here with SPFx and React:
https://github.com/SharePoint/sp-dev-fx-webparts/tree/master/samples

CanJS on click outside of component effecting component

I would like to add an event listener in in <some-component> that reacts to the button.
<some-component></some-component>
<button class="click">click here</button>
I am sure this is really simple. I am very new to CanJS and working on it.
<can-component tag="some-component">
<style type="less">
<!-- stuff -->
</style>
<template>
<!-- stuff -->
</template>
<script type="view-model">
import $ from 'jquery';
import Map from 'can/map/';
import 'can/map/define/';
export default Map.extend({
define: {
message: {
value: 'This is the side-panels component'
}
}
});
</script>
</can-component>
I tried adding a $('body').on('click', '.click', function() {}); to the component and it didn't seem to work. Been reading a lot of documentation, but I am still missing some fundamental understanding.
UPDATE
I tried this:
<some-component-main>
<some-component></some-component>
<button class="click">click here</button>
</some-component-main>
with the event listener in some-component-main
events: {
".click click": function(){
console.log("here I am");
}
},
But that also didn't work.
<some-component-main>
<some-component></some-component>
<button class="click">click here</button>
</some-component-main>
with the event listener in some-component-main
events: {
".click click": function(){
console.log("here I am");
}
},
This did work once I realized that components ending with a number causes other issues that was preventing it.
You can make things inside your component available to the parent scope using the {^property-name} or {^#method-name} syntax. Read about it here: https://canjs.com/docs/can.view.bindings.toParent.html
Here's a fiddle: http://jsbin.com/badukipogu/1/edit?html,js,output
In the following example, <my-compontent> implements a doSomething method and we the button to call that method when clicked. We expose the method as "doFooBar".
<my-component {^#do-something}="doFooBar" />
<button ($click)="doFooBar">Button</button>
and the code:
can.Component.extend({
tag: "my-component",
template: can.view('my-component-template'),
viewModel: can.Map.extend({
doSomething: function () {
alert('We did something');
}
})
});
But why does the example use ^#do-something="..." instead of ^#doSomething="..."??
DOM node attributes are case insensitive, so there's no way to tell the difference between doSomething="", DoSomEthiNg="", or DOSOMETHING="" - all three are equivalent. CanJS is following the way browsers work by converting attributes with dashes to camelCase and vice versa.
Consider native data attributes - if you do something like <div data-my-foo="my bar">, then the value is accessible via JavaScript by doing [div].dataset.myFoo (notice the camelCasing). The same applies to css properties where css uses "background-color" but javascript uses backgroundColor. CanJS is following this convention.

use parse react query results as an html tag attribute

This is my first time asking a question so I am a true SO newbie. I am currently working on a mobile app and I am using Parse React and Ratchet to build it. I have read the React documentations on FB github and apparently do not understand all enough to solve some problems. One of my problems is using the results of a Parse Query in the observe function of the declared ParseComponent as a value of a rendered react component, which in turn attempts to render the passed value as HTML. Below is the parent object:
export default class CategoryPage extends ParseComponent
{
observe(props,state){
return{
category: new Parse.Query('BusinessCategory').equalTo("objectId", this.props.categoryId)
};
}
render() {
return (
<div>
<Header text={this.data.category.objectId} back="true"/>
<div className="content">
<BusinessList categoryId={this.data.category.objectId}/>
</div>
<NavBar />
</div>
);
}
};
Notice I am passing the objectId of the category found in the Query as a text attribute of the Header React component. I am expecting Header as a child to use the passed property as follows:
var Header = React.createClass({
render: function () {
return(
<header className="bar bar-nav">
<h1 className="title">{this.props.text}</h1>
</header>
);
}
});
However the h1 is not rendering anything! I am thinking that this.data.category.objectId is a string and therefore should be rendered in the h1 tag as a string.
I do appreciate your answers very much.

Programmatically create special Polymer-Element

I have got a polymer-element with following html:
<polymer-element name="tab-bar">
<template>
<button>Hello</button>
<template repeat="{{item in items}}">
<div>This element is {{ item }}</div>
</template>
</template>
<script type="application/dart" src="tab_bar.dart"></script>
</polymer-element>
The underlying dart class looks as follows:
import 'package:polymer/polymer.dart';
#CustomTag('tab-bar')
class TabBar extends PolymerElement {
List items;
TabBar(List<String> items) {
this.items = toObservable(items);
}
}
With the following approach, it isn't possible to programmatically add the element:
query('body').children.add(createElement(new TabBar(['One','Two','Three'])));
So now, how can I add such a polymer element programatically and also set the list in the constructor?
As of polymer 0.8.5 you can use constructor like
new Element.tag('tag-bar');
also, no more .xtag and no more .host (host == this now)
credits go to Seth Ladd who explained this on polymer mailing list
Note: this only works for polymer.dart < 0.8.5. See other answer for the new way.
Custom elements don't really have constructors like we're familiar with them in Dart. Instead, try this:
var tabBar = createElement('tag-bar');
tabBar.xtag.items = toObservable(['a','b','c']);
query('body').children.add(tabBar);

Resources