How do I render components with custom menu? - javascript

I am new to HubSpot Platform, with ReactJS background so I'm used to working with components and react-router for navigation;
I came across one problem while building a website template using drag&drop module;
Basically I did not use "menu" or "advanced menu" built-in modules and decided to build navigation myself (reason was: applying custom styling, which is hard with built-in menus);
I built it, which means I have anchor tags which should change the layout after clicked; It should behave like react router: basic components of a website like Header and Footer should not change, but some modules(Components) should change depending on the url location;
I have a folder in design tools which consists of different sections; These sections include different custom modules which then I use in Drag&Drop module that is the main page that is being rendered.
Has anybody had the same problem? If so, how did you manage to make it work ?
Thanks ,
I think I made the issue clear for understanding; If not, let me know and I'll try to explain better.

var addMenu;
componentWillMount: function() {
addMenu = new nw.Menu();
addMenu.append(new nw.MenuItem({
label: 'doSomething',
click: function() {
// doSomething
}
}));
},
contextMenu: function(e) {
e.preventDefault();
addMenu.popup(e.clientX, e.clientY);
},
render: function(){
return <button onClick={this.handleClick} onContextMenu={this.contextMenu}>SomethingUseful</button>
}

Related

why Jquery not load when change route in angular 7?

I have a problem when navigate between routes with angular 7, for example... i have this code in my app-component.html
<app-navbar *ngIf="currentUser"></app-navbar>
<router-outlet></router-outlet>
<app-footer *ngIf="currentUser"></app-footer>
Everything in the page works, the <router-outlet></router-outlet> have the main component for the page, this component have inside another component, the <app-leftmenu></app-leftmenu> this is the sidebar; originally this was a html-bootstrap 3 template that i segmented for use in angular cli.
Ok the reason for this question is that the SIDEBAR uses jquery for the basic actions like collapse and expand using the next code:
//
// Sidebar categories
//
// Hide if collapsed by default
$('.category-collapsed').children('.category-content').hide();
// Rotate icon if collapsed by default
$('.category-collapsed').find('[data-action=collapse]').addClass('rotate-180');
// Collapse on click
$('.category-title [data-action=collapse]').click(function (e) {
e.preventDefault();
var $categoryCollapse = $(this).parent().parent().parent().nextAll();
$(this).parents('.category-title').toggleClass('category-collapsed');
$(this).toggleClass('rotate-180');
containerHeight(); // adjust page height
$categoryCollapse.slideToggle(150);
});
But not work in angular, only when i reload once time the component if i navigate to other route the sidebar ignore the jquery, but refreshing the page works... how i resolve this... with no refreshing? Thanks for the help!
Presumably because at the time your jQuery code is executed $('.category-collapsed'), there is no such element.
Try jQuery.on(), which allows for later binding: https://api.jquery.com/on/
So this
$('.category-title [data-action=collapse]').click(fn)
becomes (untested)
$(document).on('click', '.category-title [data-action=collapse]', fn)
I would strongly suggest using Angular-Bootstrap instead of a vanilla javascript/jquery solution.
https://ng-bootstrap.github.io/#/home
This library provides a variety of standard UI components - tabbed interfaces, dropdowns, accordion menus etc. all as native Angular components with full TypeScript support.

Gumroad Embedded Widget breaks on navigation in angular

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.

Capture event for YUI light box close

I have a light box (YUI) in my application. Upon closing of this lightbox by making use of the 'x' at the upper right side, I need to perform a set of actions. For this I need to capture the event that gets triggered when the lightbox is closed. Can some one please help?
Note - I did some research online and even went through the YUI js files but could not figure out a solution.
Unfortunately the Lightbox module in the YUI Gallery is very much outdated and it's no using any of the YUI components that would let you react to the lightbox being closed. I'd recommend that you use AUI's ImageViewer component which is pretty similar to a Lightbox. It allows you to listen to an event that signals the closing of the viewer like this:
YUI().use('aui-image-viewer', function(Y) {
var imageViewer = new Y.ImageViewer({
links: '#gallery a'
});
imageViewer.render();
imageViewer.on('visibleChange', function (Y) {
// if e.newVal is false, then the image viewer is being hidden
if (!e.newVal) {
}
});
});
It might be possible to do something like this:
Y.one('#buttonNavClose').on('click', doSomething);
buttonNavClose being the id of the close button node.

How do we set an onExpand event in a cfLayout accordion

We're using CFLayout to create a tab structure in our web application. After creation of that layout we call this function:
mytabs = ColdFusion.Layout.getTabLayout("#attributes.cflayoutName#");
mytabs.on('tabchange',
function(tablayout,tab) {
var tabtitle = tab.title;
alert(tabtitle); // Actual code does various useful 'stuff' here.
}
);
That piece of code works very well, and the alert will show each time the user clicks on a tab.
The problem is that we are now trying to do the same thing with a CFLayout type of "accordion", and I cannot get an event to fire when the user switches which accordion pane they are looking at. We've tried leaving the above as is, as well as changing the "tabchange" attribute to "expand", "beforeexpand", "activate", and "collapse".
For this testing I'm using the following simple JS function to avoid issues arising from the JS within the onchange event:
mytabs = ColdFusion.Layout.getAccordionLayout("#attributes.cflayoutName#");
mytabs.on('expand',
function(tablayout,tab) {
console.log('test');
}
);
We do not receive any errors. Nothing is logged to the console at all. I've tried replacing the console.log to an alert to rule out any problems with that line.
I found that the Ext library documentation to be very helpful with finding a solution to this problem: here.
The Ext library has a getComponent method that allows you to reference the accordion layout panel that you are trying to add the expand event to. Once you have this, you can use the "on" method you are using above to assign the expand event to each panel individually.
for (x=1; x<accordionLayoutArray.length; x++) {
mytabs.getComponent(accordionPanelName).on('expand',
function(tab) { ... });
}
This became too long for a comment so adding as an answer
After some Google searches I found what I think are some related posts. It appears as though the accordion in Ext JS does not have the same events as the tab. Instead you need to add a listener in order to catch the expanding.
See this post - in case something happens to that page here is the relevant piece:
You'd need to listen to the expand event of the child panels in the accordion, you could do something like:
Code:
myAccordion.add(myFunc('myTitle'));
function myFunc(title)
{
return new Ext.Panel(
{
title: title,
listeners: { 'expand': {fn: something, scope: foo}}
}
);
}
And I also found another similar post here on SO - see both answers
Once you know that the accordion needs a listener you can find a number of results on Google. Such as How do I attach an event handler to a panel in extJS?
This Google search will give you lots of examples.
Hope that helps.

backbone, javascript mvc - styling views with javascript

A few of my views need their textareas converted to rich text editors.
I'm using jwysiwyg as the editor. It requires that the element it is being attached to is in the page when the editor is initialized i.e. when I call $(this.el).wysiwyg(), this.el is already in the document.
Most of my views do not actually attach themselves to the dom - their render methods simply set their elements html content using the apps templating engine e.g. $(this.el).html(this.template(content)
Views/Controllers further up the chain look after actually inserting these child views into the page. At the same time, views do re-render themselves when their models change.
How do I ensure that the editor is attached to the element every time its rendered and still ensure that the editor is not attached until the element is already in the page?
Obviously I could hack something together that would work in this particular case but I would like an elegant solution that will work for all cases.
Any help would be much appreciated.
Edit: The main point here is that the solution must scale gracefully to cover multiple elements that must be styled after rendering and must not be styled until they are in the DOM
Edit: This is not an issue if I do top-down rendering but this is slow, I'd like a solution whereby I can render from the bottom up and then insert the complete view in one go at the top
Edit:
Using a combination of some of the techniques suggested below I'm thinking of doing something like the following. Any comments/critique would be appreciated.
app/views/base_view.js:
initialize: function() {
// wrap the render function to trigger 'render' events
this.render = _.wrap(this.render, function() {
this.trigger('render')
});
// bind this view to 'attach' events.
// 'attach' events must be triggered manually on any view being inserted into the dom
this.bind('attach', function() {
this.attached();
// the refreshed event is only attached to the render event after the view has been attached
this.bind('render', this.refreshed())
// each view must keep a record of its child views and propagate the 'attach' events
_.each(this.childViews, function(view) {
view.trigger('attach')
})
})
}
// called when the view is first inserted to the dom
attached: function() {
this.style();
}
// called if the view renders after it has been inserted
refreshed: function() {
this.style();
}
style: function() {
// default styling here, override or extend for custom
}
What if you used the JQuery LiveQuery Plugin to attach the editor? Such code could be a part of your template code, but not as HTML, but as Javascript associated with the template. Or you could add this globally. The code might look like this (assuming you've included the plugin itself):
$('textarea.wysiwyg').livequery(function() {
$(this).wysiwyg();
});
I have not tested this code, but in theory it should match an instance of a textarea element with a class of 'wysiwyg' when it appears in the DOM and call the wysiwyg function to apply the editor.
To adhere to DRY principle and get an elegant solution, you'll want a function dedicated to determining if a textarea has wysiwyg, let's say wysiwygAdder and add it if necessary. Then you can use underscore's wrap function to append your wysiwyg adder to the end of the render function.
var View = Backbone.View.extend({
el: '#somewhere',
initialize: function(){
_.bind(this, "render");
this.render = _.wrap(this.render, wysiwygAdder);
},
render: function(){
//Do your regular templating
return this;//allows wysiwygAdder to pick up context
}
});
function wysiwygAdder(context){
$('textarea', context).doYourStuff();
//check for presence of WYSIWYG, add here
}
When the view is initialized, it overwrites your render function with your render function, followed by wysiwygAdder. Make sure to return this; from render to provide context.
One solution would be to use event delegation and bind the focus event to check whether the rich text editor had been loaded or not. That way the user would get the text editor when they needed it (via the lazy loading, a minor performance improvement) and you wouldn't have to load it otherwise. It would also eliminate needing to worry about when to attach the rich text editor and that being dependent on the rendering chain.
If you're worried about the FOUC (flash of unstyled content) you could simply style the un-modified text areas to contain an element with a background image the looked just like the wysiwyg controls and have your focus binding toggle a class to hide the facade once the rich text editor had taken over.
Here's a sample of what I had in mind:
var View = Backbone.View.extend({
el: '#thing',
template: _.template($("#template").html()),
render: function() {
// render me
$(this.el).html(this.template(context));
// setup my textareas to launch a rich text area and hide the facade
$(this.el).delegate('focus', 'textarea', function() {
if(!$(this).hasRichTextEditor()) { // pseudocode
$(this).wysiwyg();
$(this).find('.facade').toggle();
}
});
}
});
Great problem to solve! Not too sure I've got the entire jist but... You may be able to get away with a 'construction_yard' (I just made that term up) that's way off to the left, build and place items there, then just move them when they're ready to be placed. Something along the lines of:
.construction_yard {
position: absolute;
left: -10000000000px;
}
This solution may fix several problems that might crop up. For example jquery height and width attributes on something that's 'hidden' are 0, so if you are styling along those lines, you'd have to wait till it was placed, which is more complicated, and jumbles things up.
your views would then need to do something along the lines of (pseudo-code):
//parent
//do all your view-y stuff...
foreach (child = this.my_kids) {
if child.is_ready() {
put_that_child_in_its_place();
}
}
Similarly, for children, you'd do a similar thing:
//child
foreach(parent = this.my_parents) {
if parent.isnt_ready() {
this.go_play_in_construction_yard();
} else {
this.go_to_parents_house();
}
}
... and, since backbone is pretty easy to extend, you could wrap it up in a more generalized class using:
var ParentsAndChildrenView = Backbone.View.extend({blah: blah});
and then
var Wsywig = ParentsAndChildrenView.extend({blah: blah});
hope that helps!
Almost forgot to note my source:
http://jqueryui.com/demos/tabs/#...my_slider.2C_Google_Map.2C_sIFR_etc._not_work_when_placed_in_a_hidden_.28inactive.29_tab.3F

Categories