I have a web-component at root level. The simplified version of which is shown below:
class AppLayout {
constructor() {
super();
this.noShadow = true;
}
connectedCallback() {
super.connectedCallback();
this.render();
this.insertAdjacentHTML("afterbegin", this.navigation);
}
render() {
this.innerHTML = this.template;
}
get template() {
return `
<h1>Hello</h1>
`;
}
navigation = `
<script type="module">
import './components/nav-bar.js'
</script>
`;
}
customElements.define('app-layout', AppLayout);
I want to load a script after this component loads. The script creates html for navigation and tries to add it to the app-layout element shown above. However, even though, it does find the app-layout element, it is unable to append the navBar element. It is, however, able to append the navBar to the body of the html. Any ideas what I'm missing.
const navLinks =
`<ul>
<li>Some</li>
<li>Links</li>
</ul>
`;
const navBar = document.createElement('nav');
navBar.innerHTML = navLinks;
const appLayout = document.querySelector('app-layout'); // works with 'body' but not with 'appLayout'
console.log(appLayout); // Logs correct element
appLayout.appendChild(navBar);
I know that what I'm trying to do here (loading a script inside a web component) is not ideal, however, I would like to still understand why the above doesn't work.
using innerHTML or in your case insertAdjacentHTML to add <script> tags to the document doesn't work because browsers historically try to prevent potential cross site script attacks (https://www.w3.org/TR/2008/WD-html5-20080610/dom.html#innerhtml0)
What you could do is something like:
const s = document.createElement("script");
s.type = "module";
s.innerText = `import './components/nav-bar.js'`;
this.append(s);
// or simply directly without the script: `import('./comp..')` if it is really your only js in the script tag.
I am trying to add tracking script in .js file to my angular 12 application.
(function() {
var ds = document.getElementsByTagName("script")[0];
var dm = document.createElement("img");
dm.width = 1;
dm.height = 1;
dm.alt = " ";
dm.src = "https://example.com/url=" + window.location.href;
ds.parentNode.insertBefore(dm, ds);
})();
Script fires properly on the first page, but doesn't fire on navigating to another page on my website. When i check in elements window.location.href is not changing for the new page. It is because of SPA. How can i get latest location on all pages and script to fire on all pages
You could do it the Angular way, although idk if that will work.
You can go into your root component (should be AppComponent), and simply throw this inside the constructor or ngOnInit()
#Component({
...
})
export class AppComponent {
constructor(router: Router) {
router.events.pipe(
filter(route => route instanceof NavigationEnd),
concatMap((route) => ajax('https://example.com/url=' + route.url)),
).subscribe();
}
}
Unless you need your tracker to be an Img for some reason, or you get permission / CORS issues. This should work nicely
I have an application where I need to print content that doesn't exist on the page but does exist in the angular template. In the template, I have a div with a click method that will accept an identifier, and then build an iframe based on the content of the id it received. The first snippet is what the clickable element looks like.
<div (click)="printButton('#printThis')" class="menu-item print-menu-item">Race {{ raceSelected?.raceNumber }}</div>
This code snippet is just a basic element with omitted private data. So it will just be an ng-template with an identifier and an iframe that I access from the method pasted below.
<ng-template #printThat>
// content that I will print but not important for this question
</ng-template>
<ng-template #printThis>
// content that I will print but not important for this question
</ng-template>
<iframe #iframe></iframe>
The last code snippet is the method on the component that builds the iframe and brings up the print dialog.
public async printButton(printMode: string): Promise<void> {
let printTemplate;
if (printMode === '#printThat') {
await this.getAllRacesForPrint()
printTemplate = this.printAll;
} else if (printMode === '#printThis') {
printTemplate = this.printSingleRace;
}
const iframe = this.iframe.nativeElement;
this.portalHost = new DomPortalOutlet(
iframe.contentDocument.body,
this.componentFactoryResolver,
this.appRef,
this.injector
);
const portal = new TemplatePortal(
printTemplate,
this.viewContainerRef
);
// Attach portal to host
this.portalHost.attach(portal);
iframe.contentWindow.onafterprint = () => {
iframe.contentDocument.body.innerHTML = '';
};
this._attachStyles(iframe.contentWindow);
iframe.contentWindow.print();
}
These two private functions are invoked in the method above to add the css to the iframe.
private _attachStyles(targetWindow: Window): void {
// Copy styles from parent window
document.querySelectorAll('style').forEach((htmlElement) => {
targetWindow.document.head.appendChild(htmlElement.cloneNode(true));
});
// Copy stylesheet link from parent window.
const styleSheetElement = this._getStyleSheetElement();
targetWindow.document.head.appendChild(styleSheetElement);
}
private _getStyleSheetElement(): HTMLLinkElement {
const styleSheetElement = document.createElement('link');
document.querySelectorAll('link').forEach((htmlElement) => {
if (htmlElement.rel === 'stylesheet') {
const absoluteUrl = new URL(htmlElement.href).href;
styleSheetElement.rel = 'stylesheet';
styleSheetElement.type = 'text/css';
styleSheetElement.href = absoluteUrl;
}
});
return styleSheetElement;
}
This works perfectly on all devices without a problem EXCEPT Android devices. On Android, the print dialog box comes up but the page is blank and has no content. Are there any good solutions for printing for Android that do not require bringing in a package or am I missing something here?
I have a component which displays a data. I have to open this component in a new window on clicking a button/ link from a parent component.
export default class Parent extends Component {
construtor(props) {
super(props);
}
viewData = () => {
window.open('childcomponent.js','Data','height=250,width=250');
}
render() {
return (
<div> <a onclick={this.viewData}>View Data</a></div>
)
}
}
I dont know how to invoke another component and also display it in a new size specified window.
Actually I need to send a props to that child component with which it will fetch me the data from database and render it.
You can use ReactDOM.createPortal to render a component in a new window as David Gilbertson explains in his post:
class MyWindowPortal extends React.PureComponent {
constructor(props) {
super(props);
// STEP 1: create a container <div>
this.containerEl = document.createElement('div');
this.externalWindow = null;
}
render() {
// STEP 2: append props.children to the container <div> that isn't mounted anywhere yet
return ReactDOM.createPortal(this.props.children, this.containerEl);
}
componentDidMount() {
// STEP 3: open a new browser window and store a reference to it
this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');
// STEP 4: append the container <div> (that has props.children appended to it) to the body of the new window
this.externalWindow.document.body.appendChild(this.containerEl);
}
componentWillUnmount() {
// STEP 5: This will fire when this.state.showWindowPortal in the parent component becomes false
// So we tidy up by closing the window
this.externalWindow.close();
}
}
The upvoted answer works great!
Just leaving a function component version here in case people are searching for that in the future.
const RenderInWindow = (props) => {
const [container, setContainer] = useState(null);
const newWindow = useRef(null);
useEffect(() => {
// Create container element on client-side
setContainer(document.createElement("div"));
}, []);
useEffect(() => {
// When container is ready
if (container) {
// Create window
newWindow.current = window.open(
"",
"",
"width=600,height=400,left=200,top=200"
);
// Append container
newWindow.current.document.body.appendChild(container);
// Save reference to window for cleanup
const curWindow = newWindow.current;
// Return cleanup function
return () => curWindow.close();
}
}, [container]);
return container && createPortal(props.children, container);
};
This answer is based on David Gilbertson's post. It has been modified to work in Edge. To make this work in Edge div and style elements must be created with the window into which they will be rendered.
class MyWindowPortal extends React.PureComponent {
constructor(props) {
super(props);
this.containerEl = null;
this.externalWindow = null;
}
componentDidMount() {
// STEP 1: Create a new window, a div, and append it to the window. The div
// *MUST** be created by the window it is to be appended to (Edge only)
this.externalWindow = window.open('', '', 'width=600,height=400,left=200,top=200');
this.containerEl = this.externalWindow.document.createElement('div');
this.externalWindow.document.body.appendChild(this.containerEl);
}
componentWillUnmount() {
// STEP 2: This will fire when this.state.showWindowPortal in the parent component
// becomes false so we tidy up by just closing the window
this.externalWindow.close();
}
render() {
// STEP 3: The first render occurs before componentDidMount (where we open the
// new window) so container may be null, in this case render nothing.
if (!this.containerEl) {
return null;
}
// STEP 4: Append props.children to the container <div> in the new window
return ReactDOM.createPortal(this.props.children, this.containerEl);
}
}
The full modified source can be found here https://codepen.io/iamrewt/pen/WYbPWN
You wouldn't open the component directly. You'll need a new page/view that will show the component. When you open the window, you'll then point it at the appropriate URL.
As for size, you provide it as a string in the third parameter of open, which you actually have correct:
window.open('http://example.com/child-path','Data','height=250,width=250');
Note, however, that browsers may, for a variety of reasons, not respect your width and height request. For that reason, it's probably a good idea to also apply appropriate CSS to get a space the right size in case it does open larger than you wanted.
Adding to the current answers - to copy the styles from the original window to the popup window, you can do:
function copyStyles(src, dest) {
Array.from(src.styleSheets).forEach(styleSheet => {
dest.head.appendChild(styleSheet.ownerNode.cloneNode(true))
})
Array.from(src.fonts).forEach(font => dest.fonts.add(font))
}
and then add
copyStyles(window.document, popupWindow.document);
after the call to window.open and once the ref is initialized
For anyone having problem with this answer regarding copying styles and it's not working on production build.
In case you open new window using window.open('', ...), the new window will most likely have about:blank as URL and won't find css files as they have relative paths. You need to set absolute path to href attribute instead:
function copyStyles(src, dest) {
Array.from(src.styleSheets).forEach((styleSheet) => {
const styleElement = styleSheet.ownerNode.cloneNode(true);
styleElement.href = styleSheet.href;
dest.head.appendChild(styleElement);
});
Array.from(src.fonts).forEach((font) => dest.fonts.add(font));
}
If someone is having trouble adding the styles to your new window, the trick is to copy the whole DOM head from the parent to your new popup window.
First you need to stablish the whole html skeleton
newWindow.document.write("<!DOCTYPE html");
newWindow.document.write("<html>");
newWindow.document.write("<head>");
newWindow.document.write("</head>");
newWindow.document.write("<body>");
newWindow.document.write("</body>");
newWindow.document.write("</html>");
// Append the new container to the body of the new window
newWindow.document.body.appendChild(container);
Now in the new window's DOM we have an empty head tag, we traverse the parent head tag and append its children to the new head.
const parentHead= window.document.querySelector("head").childNodes;
parentHead.forEach( item =>{
newWindow.document.head.appendChild(item.cloneNode(true)); // deep copy
})
And that's all. With the new window having the same head children than the parent, all your styles should be working now.
P.S. some styles might be a little stubborn and will not work, the hack that I found worked for those ones is to add a setTimeout in the componentDidMount or in the useEffect with the right time so as you can update your new head with the parent head. Something like this
setTimeout(() => {
updateHead();
}, 5000);
Long story short: I'm trying to add a front-end app to my portfolio site that uses React. I would like to integrate the app into the component as it renders. What I have setup right now is:
React component:
class Giphy extends Component {
constructor(props) {
super(props);
this.state = {src: 1}
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
this.setState({src: event.target.value})
}
componentDidMount() {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = "/scripts/giphyLogic.js";
document.body.appendChild(script);
}
...and a bunch of stuff in the render() method that doesn't matter
the script that I want to load involves a bunch of jQuery and simple JS stuff.
function displayButtons() {
$("#buttons").empty();
for (i=0; i<buttonArray.length; i++){
var a = $("<button type='button' class='btn btn-info'>");
var btnID = buttonArray[i].replace(/\s+/g, "+")
a.attr("id", btnID);
a.text(buttonArray[i]);
$("#buttons").append(a);
}
}
$("#addButton").on("click", function() {
var newButton = $(".form-control").val();
buttonArray.push(newButton);
displayButtons();
})
function displayGIFs() {
$(".btn-info").on("click", function() {
$("#resultsContainer").empty();
var subject = $(this).attr("id");
var giphyURL = "http://api.giphy.com/v1/gifs/search?q=" + subject + "&api_key=dc6zaTOxFJmzC";
$.ajax({ url: giphyURL, method: "GET"}).done(function(res) {
for (t=0; t<25; t++) {
var rating = res.data[t].rating;
var image = $("<img>");
var imgURLmoving = res.data[t].images.fixed_height.url;
var imgURLstill = res.data[t].images.fixed_height_still.url;
image.attr("src", imgURLstill);
image.attr("data-still", imgURLstill);
image.attr("data-moving", imgURLmoving);
image.attr("data-state", "still")
image.addClass("gif");
$("#resultsContainer").append("<p>" + rating + "</p");
$("#resultsContainer").append(image);
}
})
$(document.body).on("click", ".gif", function() {
var state = $(this).attr("data-state");
if (state === "still") {
$(this).attr("src", $(this).data("moving"));
$(this).attr("data-state", "moving");
} else {
$(this).attr("src", $(this).data("still"));
$(this).attr("data-state", "still");
}
})
})
}
displayButtons();
displayGIFs();
This all works on a standalone HTML document, but I can't seem to get the script to work properly. When the component loads and I inspect the page,
<script type="text/javascript" src="/scripts/giphyLogic.js"></script>
is there under the bundle.js script tag, but nothing from the script happens.
I get an "Uncaught SyntaxError: Unexpected token <" error that is attributed to giphyLogic.js:1 even though in the actual .js file, that line is blank. I've looked around, and this apparently happens when a file is included that doesn't exist, but the file is definitely there. I've double checked the path (by including an image in the same folder and loading the image successfully on the page) and it's correct.
Is there a way to resolve this, or am I going to have to create methods within the React component that I'm creating?
Do not mix jQuery and react. Learn how to use react properly by reading the well-written documentation. They can guide you through the many examples to get a simple app up and running.
Once again, do NOT use jQuery and react. jQuery wants to manually manipulate the DOM, and react manages a virtual DOM. The two will conflict more often than not, and you're going to have a bad time. If you have a very deep understanding of react, there are very few scenarios in which you could maybe use some jQuery, but nearly all of the time, it is to be avoided at all costs.
Obviously things like $.ajax() are fine, but for anything dealing with DOM manipulation, stay away. And if you only end up using jQuery for $.ajax() calls... you should switch to a leaner library like axios or use the native fetch API.