How do I test a route slug component in sveltekit? - sveltekit

I have this page:
src/item/[slug]/+page.svelte
trying to render it in vitest gives me an error:
...
render(item)
..
After a trial and error session I narrowed down the cause, it is because the component is a slug, the parameter doesn't get initialized in the tests.
import {page} from '$app/stores'
let param = $page.params;
let slug = param.slug
how can I initialize it in my test file?

Ok,for a work around I placed the entire contents of my +page.svelte to a new svelte component file and created an export variable for the slug value
item.svelte
<script>
export let itemId;
</script>
<div>
...
and in my slug page this is the only code remained:
+page.svelte
<script>
import {page} from '$app/stores'
let param = $page.params;
let slug = param.slug
</script>
<item bind:slug={slug}></item>
and just ran my tests on the separate component.

Related

nextjs Dynamic route rendering content not working

I am stuck on this problem for many days. I am using Next.js and have 3 pages.
pages/index.js
pages/categories.js
pages/categories/[slug].js
The categories/[slug].js is using Next.js fetching method name getServerSideProps that runs on each request and used for build dynamic pages on runtime. The categories/[slug].js is rendering a dynamic content on the page that dynamic content comes from the CMS as a response from the API Endpoint. Dynamic content is nothing but a string that contains HTML with <script /> elements.
Note: To fetch the content from the CMS we have to send a POST request with the CMS credentials like username, password, and the page slug for the content. I am using axios library to send a post request and the method is inside post.js file.
post.js:
import axios from 'axios';
const postCMS = async (slug) => {
const url = `${process.env.CMS_API_URL}/render-page/`;
let pageSlug = slug;
// If the pageSlug is not start with `/`, then create the slug with `/`
if (!pageSlug.startsWith('/')) {
pageSlug = `/${pageSlug}`;
}
const head = {
Accept: 'application/json',
'Content-Type': 'application/json'
};
const data = JSON.stringify({
username: process.env.CMS_API_USERNAME,
password: process.env.CMS_API_PASSWORD,
slug: pageSlug
});
try {
const response = await axios.post(url, data, {
headers: head
});
return response.data;
} catch (e) {
return e;
}
};
export default postCMS;
But for the rendering content on the categories/[slug].js page, I am using the Reactjs prop name dangerouslySetInnerHTML to render all the HTML which also contains <script /> elements in the JSON string.
pages/categories/[slug].js:
<div dangerouslySetInnerHTML={{ __html: result.html }} />
The content is loading fine based on each slug. But when I navigate to that category page i.e.pages/categories/index.js.
<Link href="/categories/[slug]" as="/categories/online-cloud-storage">
<a>Online Cloud Storage</a>
</Link>
It has a <Link /> element and when I click it.
The dynamic content is loading fine but that dynamic content contains accordion and slider elements they are not working. I think <script /> of these elements is not working. But when I refresh the page they work fine. See this.
They also work fine when I set the Link something like this.
<Link href="/categories/online-cloud-storage" as="/categories/online-cloud-storage">
<a>Online Cloud Storage</a>
</Link>
But after setting the link like the above method, the click is caused to hard reload the page. But I don't want this. Everything should work. When the user clicks on the category link.
Is there a way to fix this?
Why the content elements are not working when you click from the categories/index.js page?
Github repo
Code:
pages/index.js:
import React from 'react';
import Link from 'next/link';
const IndexPage = () => {
return (
<div>
<Link href="/categories">
<a>Categories</a>
</Link>
</div>
);
};
export default IndexPage;
pages/categories/index.js:
import React from 'react';
import Link from 'next/link';
const Categories = () => {
return (
<div>
<Link href="/categories/[slug]" as="/categories/online-cloud-storage">
<a>Online Cloud Storage</a>
</Link>
</div>
);
};
export default Categories;
pages/categories/[slug].js:
import React from 'react';
import Head from 'next/head';
import postCMS from '../../post';
const CategoryPage = ({ result }) => {
return (
<>
<Head>
{result && <link href={result.assets.stylesheets} rel="stylesheet" />}
</Head>
<div>
<h1>Category page CMS Content</h1>
<div dangerouslySetInnerHTML={{ __html: result.html }} />
</div>
</>
);
};
export const getServerSideProps = async (context) => {
const categorySlug = context.query.slug;
const result = await postCMS(categorySlug);
return {
props: {
result
}
};
};
export default CategoryPage;
The problem here is that <script> tags which are dynamically inserted with dangerouslySetInnerHTML or innerHTML, are not executed as HTML 5 specification states:
script elements inserted using innerHTML do not execute when they are inserted.
If you want to insert new <script> tag after the page has initially rendered, you need to do it through JavaScript's document.createElement('script') interface and appended to the DOM with element.appendChild() to make sure they're executed.
The reason why the scripts don't work after changing routes, but they do work after you refresh the page is tied to Next.js application lifecycle process.
If you refresh the page, Next.js pre-renders the entire website on the server and sends it back to the client as a whole. Therefore, the website is parsed as a regular static page and the <script> tags are executed as they normally would.
If you change routes, Next.js does not refresh the entire website/application, but only the portion of it which has changed. In other words, only the page component is fetched and it is dynamically inserted into existing layout replacing previous page. Therefore, the <script> tags are not executed.
Easy solution
Let some existing library handle the hard work for you by parsing the HTML string and recreating the DOM tree structure. Here's how it could look in jQuery:
import { useEffect, useRef } from 'react';
import $ from 'jquery';
const CategoryPage = ({ result }) => {
const element = useRef();
useEffect(() => {
$(element.current).html($(result.html));
}, []);
return (
<>
<Head>
{result && <link href={result.assets.stylesheets} rel="stylesheet" />}
</Head>
<div>
<h1>Category page CMS Content</h1>
<div ref={element}></div>
</div>
</>
);
};
export const getServerSideProps = async (context) => {
/* ... */
return { props: { result } };
}
Harder solution
You would have to find a way to extract all <script> tags from your HTML string and add them separately to your page. The cleanest way would be to modify the API response to deliver static HTML and dynamic script in two separate strings. Then, you could insert the HTML with dangerouslySetInnerHTML and add script in JS:
const scripts = extractScriptTags(result.html); // The hard part
scripts.forEach((script) => {
const scriptTag = document.createElement('script');
scriptTag.innerText = script;
element.current.appendChild(scriptTag);
});
IMHO, I believe that the script non-proper loading is due to erroneous import of the scripts on Client-Side Rendering (CSR) and Server-Side Rendering (SSR). Read more here, but also take a look on this interesting article. This would also explain the problematic behavior of your link component.
In your case, I understand that this behavior is due to false handling of the lifecycle of the page and its components during CSR, as many scripts might need to properly shared across the navigation of pages, possibly in SSR. I do not have the full picture of what the problem is or extended expertise on NextJS, but I believe that those scripts should be imported in one place and possibly rendered on the server, instead of false importing on each page, falsely letting CSR do the work in a non-NextJS optimized manner.
The suggested way is to use a custom Document implementation (SSR-only) for your application, where you can define the scripts. See here for more details on this. Also I suppose you already have setup a custom App file for your App, where you will use it in your document, for both CSR and SSR rendering common to all pages (See this SO question for more on that).
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
// ...
}
render() {
return (
<Html>
<Head>
{/*Your head scripts here*/}
</Head>
<body>
<Main />
{/*Your body scripts here*/}
<NextScript />
</body>
</Html>
)
}
}
The Head native component does a lot of work on the background in order to setup things for scripts, markup, etc. I suggest you go that way, instead of just adding the scripts into each page directly.

getInitialProps is never executed

I'm trying to make a Twitter component that loads the tweet from the twitter API and displays its HTML, which will be a <blockquote> and a <script> tag.
The advantage of this, if server side rendered, is that it would work even if the user has privacy settings that block the call to the twitter script: the user will still get the blockquote, which is better than the current behavior (shows nothing).
So, my idea was to do something like this:
import fetch from 'node-fetch'
function Tweet(props) {
return <>
<div dangerouslySetInnerHTML={{
__html: props.__html
}} />
<p>here I am</p>
</>
}
Tweet.getInitialProps = async ctx => {
console.log("here I am on the road again")
const res = await fetch(`https://api.twitter.com/1/statuses/oembed.json?id=${ctx.tweetId}`)
const json = await res.json()
return { __html: json.html }
}
export default Tweet
And then on the pages use it like <Tweet tweetId="blah" />.
I found two problems with my idea though:
As per docs I cant access the tweetId property on getInitialProps
getInitialProps is never called. The <p>here I am</p> appears on the HTML and the log is never printed anywhere.
So, my question is: what I am doing wrong? Is this even possible to do?
Thanks!
As per Next.js documentation, getInitialProps can only be used to a page, and not in a component:
getInitialProps can only be added to the default component exported by a page, adding it to any other component won't work.

React only renders embedded app on direct navigation, not when routed to page

I have a simple component that renders an embedded Calendly app:
class BookAppointment extends Component {
render(){
const data_url = `https://calendly.com/username?name=${this.props.firstName}%20${this.props.lastName}&email=${this.props.email}`
return (
<div class="calendly-inline-widget" data-url={data_url} style={{"min-width":"320px","height":"780px"}} />
);
}
}
However, the Calendly widget only appears if I navigate directly to this route (via typing in the url). If I navigate here via clicking a NavBar link and having my react-router-dom route me, it doesn't load. Why might this be the case?
I previously put the script file in the <head> of my index.html file. Unfortunately, to solve this, I had to modify the DOM directly, appending and removing the widget dynamically when the component mounted and unmounted:
.
.
.
componentDidMount() {
const head = document.querySelector('head');
const script = document.createElement('script');
script.id = "calendly-widget";
script.setAttribute('src', 'https://assets.calendly.com/assets/external/widget.js');
head.appendChild(script);
}
componentWillUnmount() {
const calendlyWidget = document.getElementById("calendly-widget");
const head = document.querySelector("head");
head.removeChild(calendlyWidget);
}
I'm not really sure about why I had to do this. If anyone could comment with the reason, I think that would enhance the quality of this answer for others.

Read the current full URL with React?

How do I get the full URL from within a ReactJS component?
I'm thinking it should be something like this.props.location but it is undefined
window.location.href is what you're looking for.
If you need the full path of your URL, you can use vanilla Javascript:
window.location.href
To get just the path (minus domain name), you can use:
window.location.pathname
console.log(window.location.pathname); //yields: "/js" (where snippets run)
console.log(window.location.href); //yields: "https://stacksnippets.net/js"
Source: Location pathname Property - W3Schools
If you are not already using "react-router" you can install it using:
yarn add react-router
then in a React.Component within a "Route", you can call:
this.props.location.pathname
This returns the path, not including the domain name.
Thanks #abdulla-zulqarnain!
window.location.href is what you need. But also if you are using react router you might find useful checking out useLocation and useHistory hooks.
Both create an object with a pathname attribute you can read and are useful for a bunch of other stuff. Here's a youtube video explaining react router hooks
Both will give you what you need (without the domain name):
import { useHistory ,useLocation } from 'react-router-dom';
const location = useLocation()
location.pathname
const history = useHistory()
history.location.pathname
this.props.location is a react-router feature, you'll have to install if you want to use it.
Note: doesn't return the full url.
Plain JS :
window.location.href // Returns full path, with domain name
window.location.origin // returns window domain url Ex : "https://stackoverflow.com"
window.location.pathname // returns relative path, without domain name
Using react-router
this.props.location.pathname // returns relative path, without domain name
Using react Hook
const location = useLocation(); // React Hook
console.log(location.pathname); // returns relative path, without domain name
You are getting undefined because you probably have the components outside React Router.
Remember that you need to make sure that the component from which you are calling this.props.location is inside a <Route /> component such as this:
<Route path="/dashboard" component={Dashboard} />
Then inside the Dashboard component, you have access to this.props.location...
Just to add a little further documentation to this page - I have been struggling with this problem for a while.
As said above, the easiest way to get the URL is via window.location.href.
we can then extract parts of the URL through vanilla Javascript by using let urlElements = window.location.href.split('/')
We would then console.log(urlElements) to see the Array of elements produced by calling .split() on the URL.
Once you have found which index in the array you want to access, you can then assigned this to a variable
let urlElelement = (urlElements[0])
And now you can use the value of urlElement, which will be the specific part of your URL, wherever you want.
To get the current router instance or current location you have to create a Higher order component with withRouter from react-router-dom. otherwise, when you are trying to access this.props.location it will return undefined
Example
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
class className extends Component {
render(){
return(
....
)
}
}
export default withRouter(className)
Read this I found the solution of React / NextJs. Because if we use directly used the window.location.href in react or nextjs it throw error like
Server Error
ReferenceError: window is not defined
import React, { useState, useEffect } from "react";
const Product = ({ product }) => {
const [pageURL, setPageURL] = useState(0);
useEffect(() => {
setPageURL(window.location.href);
})
return (
<div>
<h3>{pageURL}</h3>
</div>
);
};
Note:
https://medium.com/frontend-digest/why-is-window-not-defined-in-nextjs-44daf7b4604e#:~:text=NextJS%20is%20a%20framework%20that,is%20not%20run%20in%20NodeJS.
As somebody else mentioned, first you need react-router package. But location object that it provides you with contains parsed url.
But if you want full url badly without accessing global variables, I believe the fastest way to do that would be
...
const getA = memoize(() => document.createElement('a'));
const getCleanA = () => Object.assign(getA(), { href: '' });
const MyComponent = ({ location }) => {
const { href } = Object.assign(getCleanA(), location);
...
href is the one containing a full url.
For memoize I usually use lodash, it's implemented that way mostly to avoid creating new element without necessity.
P.S.: Of course is you're not restricted by ancient browsers you might want to try new URL() thing, but basically entire situation is more or less pointless, because you access global variable in one or another way. So why not to use window.location.href instead?

Rendering a string as React component

I want to render the react components with a string as the input which i am receiving dynamically from another page. BUt i will have the references for the react components.
Here is the example
Page1:
-----------------------------
loadPage('<div><Header value=signin></Header></div>');
Page2:
--------------------------------
var React =require('react');
var Header = require('./header');
var home = React.createClass({
loadPage:function(str){
this.setState({
content : str
});
},
render : function(){
return {this.state.content}
}
});
In this example i am receiving Header component as string , and i have the reference of Header component in my receiving page . How can i substitute the string with the actual react component
This react-jsx-parser component looks like it will solve your problem
To render a react component using a string you can use.
var MyComponent = Components[type + "Component"];
return <MyComponent />;
For more information check the response here :
React / JSX Dynamic Component Name
If you can live with having all your components in one module, then this works pretty well:
import * as widgets from 'widgets';
var Type = widgets[this.props.componentId];
...
<Type />
The wildcard import works like a cut-rate component registry.
Built-in way
Without any package You can use the built in react attribute dangerouslySetInnerHTML to pass your string, and it will render it as an HTML
function Component() {
const stringElement = "<h1> My Title </h1>";
return (
<div dangerouslySetInnerHTML={{ __html: stringElement }}>
</div>
);
}
And it would work fine.
But try to avoid rendering text as much as possible, since it might expose you to XSS attacks, you should XSS clean the text or avoid this unless you have no choice
one more way to use component
const a = {
b: {
icon: <component props />
}
}
In render function
return(
{a.b.icon}
)
This will render your component in your JSON object
This string-to-react-component library can help you to achieve your desired functionality

Resources