Gumroad Embedded Widget breaks on navigation in angular - javascript
I want to include Gumroad's Embed Widgets in my Angular application.
What I've tried:
I added this to my index.html:
<script src="https://gumroad.com/js/gumroad-embed.js"></script>
I added this to one of my components' template:
<div class="gumroad-product-embed" data-gumroad-product-id="demo">Loading...</div>
The problem:
Let's say the component on page /buy. If I open /buy, the widget appears. However, if I've navigated to /buy through some routerLink in the app, the widget doesn't appear, it just keeps showing "Loading...".
I also confirmed this by changing routerLink to href and it worked. I.e., the problem is probably related to removal and re-adding of the div.
Anyway, my question: how to use Gumroad's Embed Widget inside an Angular application correctly?
Notes:
1- It could be useful to look at gumroad-embed.js. It has stuff like MutationObserver but I couldn't follow along.
2- The Angular application is running on electronjs, in case that'll make a difference (due to MutationObserver or anything else).
Till someone comes up with a better answer, here's a solution that works specifically with the current gumroad-embed.js. In the future, it could change and the answer would be obsolete. Anyway, here it is:
Solution
1) Add a file edited-gumroad-embed.js under your /assets containing the following code:
function createGumroadEmbed(){window.GumroadEmbed||(window.GumroadEmbed=new GumroadEmbedManager)}function receiveMessage(t){var e={};if(t.data)try{e=JSON.parse(t.data)}catch(r){}if("GumroadEmbedMessage"===e.type&&GumroadEmbed){var i=GumroadEmbed.findEmbed(e.args.id)||GumroadEmbed.findEmbed(e.args.unique_id);i&&("setHeight"===e.action?i.setHeight(e.args.height):"scrollToTop"===e.action&&i.scrollToTop())}}!function(){var n=!1,a=/xyz/.test(function(){})?/\b_super\b/:/.*/;this._GumroadClass=function(){},_GumroadClass.extend=function(t){function e(){!n&&this.init&&this.init.apply(this,arguments)}var o=this.prototype;n=!0;var i=new this;for(var r in n=!1,t)i[r]="function"==typeof t[r]&&"function"==typeof o[r]&&a.test(t[r])?function(i,r){return function(){var t=this._super;this._super=o[i];var e=r.apply(this,arguments);return this._super=t,e}}(r,t[r]):t[r];return e.prototype=i,(e.prototype.constructor=e).extend=arguments.callee,e}}();var GumroadClass=_GumroadClass.extend({setEnvironment:function(){this.environment="production",this.domain="https://gumroad.com",this.isMobile=navigator.userAgent.match(/Mobile[\/; ]/i)||navigator.userAgent.match(/Opera (Mini|Mobi)/i)||navigator.userAgent.match(/IEMobile/i),this[this.environment]=!0,this.origin=window.location.protocol+"//"+window.location.hostname+(window.location.port?":"+window.location.port:"")},startNodeAdditionObserver:function(){MutationObserver&&(this.nodeAdditionObserver=new MutationObserver(function(t){for(var e=0;e<t.length;e++)for(var i=0;i<t[e].addedNodes.length;i++)this.nodeAdditionCallback&&this.nodeAdditionCallback(t[e].addedNodes[i])}.bind(this)),this.nodeAdditionObserver.observe(document.body,{childList:!0,subtree:!0}))}}),GumroadEmbedElement=GumroadClass.extend({init:function(t,e){this.manager=e;var i=t.getAttribute("data-gumroad-product-id");i&&(this.div=t,this.id=i,this.opts={as_embed:"true",referrer:document.referrer,origin:this.manager.origin},this.manager.embeds.push(this),this.show())},buildUrl:function(){var t=(this.manager.domain||"")+"/l/"+this.id+"?";for(var e in this.outboundEmbed&&(this.opts.outbound_embed="true"),this.opts)this.opts.hasOwnProperty(e)&&(t+="&"+e+"="+this.opts[e]);return t},createIframe:function(){this.iframe=document.createElement("iframe"),this.iframe.allowtransparency=!0,this.iframe.setAttribute("allowFullScreen","allowfullscreen"),this.iframe.className="gumroad-embed-iframe",this.iframe.scrolling="no",this.iframe.width="100%",this.iframe.height=0,this.iframe.id="gumroad-embed-iframe-"+this.id,this.iframe.setAttribute("style","display: block !important; border: none !important; margin: 0 auto !important; padding: 0 !important; max-width: 676px !important;"),this.div.parentNode.insertBefore(this.iframe,this.div)},scrollToTop:function(){this.iframe&&this.manager.isMobile&&window.scrollTo(0,this.iframe.offsetTop)},setHeight:function(t){this.div.style.display="none",this.iframe.setAttribute("height",t)},show:function(){this.iframe||this.createIframe();this.id=this.div.getAttribute("data-gumroad-product-id"),this.outboundEmbed=!!this.div.getAttribute("data-outbound-embed"),this.iframe.setAttribute("src",this.buildUrl())}}),GumroadEmbedManager=GumroadClass.extend({init:function(){this.setEnvironment(),this.createEmbeds()},createEmbeds:function(){this.embeds=[];for(var t=document.getElementsByClassName("gumroad-product-embed"),e=0;e<t.length;e++)new GumroadEmbedElement(t[e],this)},findEmbed:function(t){for(var e=0;e<this.embeds.length;e++)if(this.embeds[e].id==t)return this.embeds[e];return!1},gotMessage:function(t){var e={};try{e=JSON.parse(t.data)}catch(i){}this[e.action]&&this[e.action](e.args)},reload:function(){for(var t=0;t<this.embeds.length;t++){var e=this.embeds[t].iframe;e&&e.parentNode&&(e.parentNode.removeChild(e),this.embeds[t].div.style.display="")}this.createEmbeds()},scrollToTop:function(t){var e=this.findEmbed(t);e&&e.scrollToTop()},setHeight:function(t,e){var i=this.findEmbed(t);i&&i.setHeight(e)}});window.addEventListener?(window.addEventListener("message",receiveMessage,!1)/*,window.addEventListener("load",createGumroadEmbed)*/):window.attachEvent&&(window.attachEvent("onmessage",receiveMessage,!1)/*,window.attachEvent("onload",createGumroadEmbed)*/);createGumroadEmbed();
2) In the component whose template contains <div class="gumroad-product-embed" data-gumroad-product-id="demo">Loading...</div>, add the following code:
import {AfterContentInit, Component, OnDestroy} from '#angular/core';
#Component({
selector: 'app-buy',
templateUrl: './buy.component.html'
})
export class BuyComponent implements OnDestroy, AfterContentInit {
readonly scriptNode: HTMLScriptElement;
constructor() {
this.scriptNode = document.createElement('script')
this.scriptNode.setAttribute('src','/assets/edited-gumroad-embed.js')
}
ngAfterContentInit() {
document.getElementsByTagName('head')[0].appendChild(this.scriptNode)
}
ngOnDestroy() {
this.scriptNode.remove()
delete window['GumroadEmbed']
}
}
3) Success!
Explanation
By looking into gumroad-embed.js, it seems, as of Nov. 2018, to be just including another file. This other file's code is the base on which edited-gumroad-embed.js is based. edited-gumroad-embed.js is basically this file with 2 edits:
It calls createGumroadEmbed() instead of calling it on load event directly, because load seems to be firing once only at initial load of document.
It comments the event listener for load that used to call createGumroadEmbed().
For the component ts file, it basically tries to simulate the loading of the script as much as possible as if it was first load. By taking a non-thorough look into gumroad's code, it seems like for actions to happen window.GumroadEmbed needs to be undefined; that's why it deletes window['GumroadEmbed'].
Open questions and caveats
There's are enough open questions to pass a camel through.
Is all of that even needed or should have original Gumroad's code succeeded had it been run on Chrome instead of electronjs?
I didn't take enough look into gumroad's code to know if what I'm doing has no side effects. For example, are there leaks? are there event listeners that should be removed? What about the MutationObserver(s)?
I'm not sure why I'm calling the code in ngAfterContentInit. I'm trying to make it run after the div has been added to the DOM as much as possible.
If the component is reused (for example, in routing), does one need to re-create the scriptNode? I currently have routing reuse disabled anyway for other reasons.
That said, I'm probably not intending to use it after all, but the reason is unrelated to the original question. The reason is that I found many requests in the Network tab to many websites (e.g., Facebook) and I don't know the effect of that on my customers (e.g., privacy-wise). Again: I do not know, I'm not familiar with iframes. Also, I'm a bit afraid that my solution might have any leaks.
I'll just add a hyperlink.
I had the same problem in React, and came up with this simple solution:
index.html:
<script src="https://gumroad.com/js/gumroad-embed.js"></script>
<div style="display:none;" id="gumroad-product-embed" class="gumroad-container">
<div class="gumroad-product-embed" data-gumroad-product-id="gwIwi">
Loading...
</div>
</div>
<script defer>
const embed = document.getElementById('gumroad-product-embed')
window.GUMROAD = embed
</script>
<gumroadcomponent>.js:
const gumroad = window.GUMROAD
const container = React.createRef()
export class BuyNUNISYNTH extends React.Component {
componentDidMount() {
gumroad.style.display = "block"
container.current.appendChild(gumroad)
}
render() {
return <div>
<MyNav no_buy_button/>
<div ref={container}></div>
</div>
}
}
This keeps the Gumroad embed outside of React so that it only needs to load once. It was the re-rendering of React that was causing the problem.
Related
vanilla-tilt only works after app has hot-updated once
I use vanilla-tilt in my svelte app. When it loads the first time, it does seem to load tilt, as the item tilts ever so slightly. Although the max tilt I've defined doesn't load. Then, if I change something arbitrary in my file so a hot update is triggered - voila, the tilt works as expected. I've tried variations of calling the Tilt.init function, in the beginning of onMount, end of onMount, after a timeout of the page loads... What to do?
I use the tilt library in my Sveltekit project and defined it as a simple action. With Svelte actions, you always know the node will be available, the moment you initialize your code. This is the main reason I recommend this approach. effects.js import VanillaTilt from 'vanilla-tilt' export function tilt (node, options) { VanillaTilt.init(node, options) return { destroy() { VanillaTilt.destroy() } }; } In your component, just use the action as you're used to component.svelte <script> import { tilt } from "./effects.js" </script> <div use:tilt> <!-- your tilting content --> </div>
Handling onClick of a <a> tag inside of dangerouslySetInnerHTML/regular html
I am currently writing a reddit client inside of ReactJS plus electron. I am simply doing this to understand apis better and to understand programming a large scale program. However I am becoming stuck when trying to render markdown. I have imported markdown to jsx libraries but none of them (unless I am using them wrong) have allowed me to properly convert the reddit markdown into a component I can work with. For example from the reddit api, I receive a comment like this: Hi mi name is John Doe, and I want you guys to check out [Reddit](http://www.reddit.com) Also don't be afraid to check out [Google](http://www.google.com) Now I have tried using npm modules that convert markdown into jsx, this works and will render the markdown, but I need to be able to interact with the onClick methods. Simple because this is running in an electron window, and navigating the whole browser window will navigate away from my program. Current I have it working with the following code, but this is very slow and calls the webview over 100 times. ... // On click of an a, this gets called a couple hundred times. :( componentDidMount() { var self = this; $('.linkHandler a').click(function (e) { e.preventDefault(); // Opens my own custom electron webview self.props.loadURL(e.target.href) })} ... render(){ return( <div className="linkHandler"> {<ReactMarkdown source={comment.body} />} </div> ) } } I just want to be able to add an onClick handler to all of the tags in my markdown. Reddit also supplies the html of the markdown already, but alas I am still in the same spot. With the html version I also tried doing the following: onClick(){ console.log('onClick Called'); } ... getHTMl(){ // Quickly tpyed this from memory, just keep in mind the original replaced every <a with <a onClick="" return comment.body_html.replace(/<a/g,'<a onClick="' + this.onClick + '"'); } ... render(){ return( <div setInnerHTMLDangerously={{"__html":this.getHTML()}} ) } } Tried adding () to the onClick but it gets called (100s of times) before clicking the button. The way it is now, it returns error: null reference (or something like that) in the console on click.
How do I import html into another html document?
I'm trying to keep my nav in one html file rather than copying and pasting it into every file so I don't have to edit every file if I want to change something. I want to include the nav code into my files but nothing I've tried so far has worked the way I want it to. I would like to do this using only html/css/js, this is something that seems like there would be an easy way to do it because it's so practical in a lot of projects. So far I've tried object/iframe - Embedded the code into it's mini window, not the desired result. javascript object.write - Deleted code already in file being imported to. w3.includehtml - Works in firefox, but not chrome, I can't figure out why. Help with this would be appreciated as this seems like the best method. php include- Didn't work, probably because I don't know php and most likely did something wrong, I'm open to it if someone could show me how or link a tutorial.
create the component as a template, like: nav.template.html <template> <nav><!-- nav stuff here ... there has to be only one childNode of template because of how we will mount it, but you could change that --> <style scoped>/* you can style only nav content directly here if you like */</style> </nav> </template> Then, load that with javascript on domready, like: site.js document.addEventListener('DOMContentLoaded', function() { // there are more thorough, extensible ways of attaching to this event .. let el = document.querySelector('.nav-mount-point') // I'm doing to use axois, assuming you load it from a CDN or directly. You could also do this with plain AJAX, etc axios.get('path/to/nav.template.html') .then(res => { let dt = document.createElement('div') dt.innerHTML = res.data el.appendChild(dt.children[0].content.cloneNode(true)) }) .catch(e => console.error(e)) }, false);
How can I change the routing in Angular from outside the scope of the Angular application?
My question title might be a bit confusing, so hopefully the following details will clear it up. Essentially, the navigation bar is out of my control and it is written in just plain HTML/JS. My application is written in Angular and has routing set up within it. Is there anything I can do to trigger routing in my Angular app from the nav bar? Say I have the following in index.html: <body> <header> <a onclick="history.pushState({}, '', '/home');">Home</a> <a onclick="history.pushState({}, '', '/test');">Test</a> </header> <app-root></app-root> </body> Obviously, my Angular application starts from <app-root> and does not know about the tag right above it. However, is there a way to affect the routing within the Angular from outside of it? I figured that calling history.pushState() would change it, but it doesn't seem to be doing anything. It does change the URL, but the component displayed on the browser stays the same. It does not switch the component. Does anyone have a solution to this problem? I really appreciate the help!
If someone is looking for a potential solution, here's a solution I was recommended in another forum. Maybe there's a better way to do it and improve upon it, but here's what I have so far: In app.component.ts (or wherever you want to handle routing): declare var window: any; //#Component stuff... export class AppComponent implements OnInit, OnDestroy { routerSubject: Subject<string> = new Subject<string>(); constructor(private router: Router) { // Assign the observable to the window object, so that we can call it from outside window.routerSubject = this.routerSubject; } ngOnInit() { this.routerSubject.subscribe((url: string) => { // Programmatically navigate whenever a new url gets emitted. this.router.navigate([`/${url}`]); }); } ngOnDestroy() { this.routerSubject.unsubscribe(); } } Now, in index.html: <body> <header> <!-- Emit a new value on click, so that Angular (which has already been subscribed) can programmatically route --> <a onclick="window.routerSubject.next('home');">Home</a> <a onclick="window.routerSubject.next('about');">About</a> </header> <app-root></app-root> </body> Obviously, you can put your onclick method in a seperate JS file, and put the Angular routing logic in a service, etc etc. But this is the general gist of the solution I came up with. Either way, this should enable people to route an Angular app from the outside.
If you are using the latest angular2 version you can use routerLink as shown below: <a routerLink="/home" routerLinkActive="my-link-active-css-class">Home</a> The documentation is pretty good: https://angular.io/docs/ts/latest/guide/router.html
Is there any in GWT to get notification about which fragment is going to be downloaded
Our GWT application has various fragments and each of them are very big in size(1+ MB). What we would like to do is to show a progress bar when GWT is downloading the fragment. We are using GWTP based code splitting. I could not find anything related to fragment loading event in GWT source code. Does anyone have any idea about how Javascript on page can be notified about which fragment is going to be downloaded next?
There is not any direct way, but you can workaround that basically in two ways 1.- Extending the CrossSiteIframeLinker and overriding the method getJsInstallScript() so as you can return a customised javascript content which could alert somehow to the user about the permutation being loaded or simply show a spinner. Note that your script should contain a code similar to installScriptEarlyDownload.js or installScriptDirect.js // Your linker class public class MyXSILinker extends CrossSiteIframeLinker { #Override protected String getJsInstallScript(LinkerContext context) { return "/your_namespace/your_script.js"; } } // your "/your_namespace/your_script.js" script function installScript(filename) { } // your module file <define-linker name="mylinker" class="...MyXSILinker" /> <add-linker name="mylinker" /> 2.- Using the window.__gwtStatsEvent function. You can define a function in your index.html to start a loading spinner when the script loading the app is going to be added to your document, then use your gwt code to remove that. // In your index.html <head> <script> window.__gwtStatsEvent = function(r) { if (r.type == 'scriptTagAdded') { d = document.createElement('div') d.id = 'loading' d.innerHTML = 'Loading...' document.body.appendChild(d); } } </head> </script> // In your EntryPoint class Document.get().getElementById("loading").removeFromParent(); To show an accurate progress-bar is quite difficult because <script> tag does not support progress events. So the best approach should be a linker loading the permutation using ajax, so as you can use html5 progress events in the xhr object. I would use the #2 approach showing a nice spinner animated with css.