I am trying to build a SPA with vanilla JS and Express server. The home view menu is a SVG file with some clickable areas to navigate to other views. When I first click on the SVG about link, the event is fired and I reach the About view. But when I go back to the home page, the links doesn't fire any click event. I guess this has to be a problem with the DOMContentLoaded event but I tried other events and could not find a solution. Any help is appreciated!
Home.js
import AbstractView from "./AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params);
this.setTitle("Home");
}
async getHtml() {
return `
<div id="molecule">
<svg version="1.1" id="calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 120.05 131.14" style="enable-background:new 0 0 150 150;" xml:space="preserve">
<a class='molecule-link about-link' href="/about" data-link>
<rect id="about-link" x="3" y="5" fill="#fff" opacity="0" width="30" height="30"></rect>
<text x="35" y="20" font-family="Inter" opacity="100" font-size="0.2rem" fill="#28ABE2">About</text>
</a>
</svg>
</div>
About.js
import AbstractView from "./AbstractView.js";
export default class extends AbstractView {
constructor(params) {
super(params);
this.setTitle("About");
}
async getHtml() {
return `
<div id = "about-view">
<h1>Hello About</h1>
<a class="view-link" href="/" data-link> LINK TO HOME </a>
</div>
`;
}
}
a short version of index.js
import Home from "./views/Home.js";
import About from "./views/About.js";
const navigateTo = url => {
history.pushState(null, null, url);
router();
}
const router = async () => {
const routes = [
{ path: "/", view: Home },
{ path: "/projects", view: Projects },
{ path: "/projects/:id", view: ProjectView },
{ path: "/about", view: About },
{ path: "/contact", view: Contact },
];
document.querySelector("#app").innerHTML = await view.getHtml();
};
window.addEventListener("popstate", router);
document.addEventListener("DOMContentLoaded", () => {
router();
});
// VIEWS NAVIGATION
document.addEventListener('DOMContentLoaded', () => {
document.body.addEventListener('click', e => {
if (e.target.matches('[data-link]')) {
e.preventDefault();
navigateTo(e.target.href);
}
})
router();
})
// MENU
document.addEventListener("DOMContentLoaded", function (event) {
if (location.pathname === "/") {
document.querySelector('.about-link').addEventListener("click", e => {
if (e.currentTarget.matches("[data-link]")) {
e.preventDefault();
explode(6, 6)
var target = e.currentTarget.href.baseVal;
setTimeout(function () {
navigateTo(target);
}, 2000);
const textLinks = document.querySelectorAll('text');
gsap.to(textLinks, {
duration: 2,
opacity: 0
})
}
});
}
});
Then the index.html file has just a <div id="app"></div> where the views are dynamically inserted.
Related
I have this component:
import React, { lazy, Suspense } from 'react';
import { ErrorBoundary } from '../ErrorBoundary';
const FALLBACK = <svg aria-label="" data-testid="icon-fallback" viewBox="0 0 21 21" />;
const ERROR = (
<svg data-testid="icon-notdef" viewBox="0 0 21 21">
<path d="M0.5,0.5v20h20v-20H0.5z M9.1,10.5l-6.6,6.6V3.9L9.1,10.5z M3.9,2.5h13.2l-6.6,6.6L3.9,2.5z M10.5,11.9l6.6,6.6H3.9 L10.5,11.9z M11.9,10.5l6.6-6.6v13.2L11.9,10.5z" />
</svg>
);
export const Icon = ({ ariaLabel, ariaHidden, name, size }) => {
const LazyIcon = lazy(() => import(`../../assets/icons/${size}/${name}.svg`));
return (
<i aria-hidden={ ariaHidden }>
<ErrorBoundary fallback={ ERROR }>
<Suspense fallback={ FALLBACK }>
<LazyIcon aria-label={ ariaLabel } data-testid="icon-module" />
</Suspense>
</ErrorBoundary>
</i>
);
};
I’m trying to test the condition where an SVG is passed in that doesn’t exist, in turn rendering the <ErrorBoundary /> fallback. The ErrorBoundary works in the browser, but not in my test.
This is the failing test:
test('shows notdef icon', async () => {
const { getByTestId } = render(<Icon name='doesnt-exist' />);
const iconModule = await waitFor(() => getByTestId('icon-notdef'));
expect(iconModule).toBeInTheDocument();
});
I get this error message:
TestingLibraryElementError: Unable to find an element by: [data-testid="icon-notdef"]”.
How do I access ErrorBoundary fallback UI in my test?
Edit
This is the code for the <ErrorBoundary /> component:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
error: '',
errorInfo: '',
hasError: false,
};
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error({ error, errorInfo });
this.setState({ error, errorInfo });
}
render() {
const { children, fallback } = this.props;
const { error, errorInfo, hasError } = this.state;
// If there is an error AND a fallback UI is passed in…
if (hasError && fallback) {
return fallback;
}
// Otherwise if there is an error with no fallback UI…
if (hasError) {
return (
<details className="error-details">
<summary>There was an error.</summary>
<p style={ { margin: '12px 0 0' } }>{error && error.message}</p>
<pre>
<code>
{errorInfo && errorInfo.componentStack.toString()}
</code>
</pre>
</details>
);
}
// Finally, render the children.
return children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
fallback: PropTypes.node,
};
… and this is the full error with DOM that I get for the test:
shows notdef icon
TestingLibraryElementError: Unable to find an element by: [data-testid="icon-notdef"]
<body>
<div>
<i
aria-hidden="false"
class="Icon Icon--sm"
>
<span
aria-label=""
data-testid="icon-module"
/>
</i>
</div>
</body>
<html>
<head />
<body>
<div>
<i
aria-hidden="false"
class="Icon Icon--sm"
>
<span
aria-label=""
data-testid="icon-module"
/>
</i>
</div>
</body>
</html>Error: Unable to find an element by: [data-testid="icon-notdef"]
Lastly, my SVG mock:
import React from 'react';
const SvgrMock = React.forwardRef(
function mySVG(props, ref) {
return <span { ...props } ref={ ref } />;
},
);
export const ReactComponent = SvgrMock;
export default SvgrMock;
As discussed in the comments, it is most likely the mock is avoiding the error. Try re mocking the SVG files with a new mock throwing an error.
// tests that require unmocking svg files
describe('non existent svg', () => {
beforeAll(() => {
jest.mock('.svg', () => {
throw new Error('file not found')
});
});
test('shows notdef icon', async () => {
const { getByTestId } = render(<Icon name='doesnt-exist' />);
const iconModule = await waitFor(() => getByTestId('icon-notdef'));
expect(iconModule).toBeInTheDocument();
});
afterAll(() => jest.unmock('.svg'))
})
It is necessary to wrap it to ensure the SVG files are re-mocked only during the test (beforeAll - afterAll) to not interfere with the rest of the tests.
I am using a composition of web components which is correctly rendered in firefox and safari but not in chrome. Indeed, sometimes the nested components are not displayed in this browser and I cannot figure out why and how to solve the problem.
here is the code:
index.html fetches data from a json file and displays the 2 components accordingly.
<html>
<head>
<meta charset="UTF-8">
<script type="module" src="/rect-shape.js"></script>
<script type="module" src="/shape-container.js"></script>
</head>
<body>
<p>rect-shape:</p>
<script>
const innerEl = document.createElement('rect-shape');
document.body.appendChild(innerEl);
fetch('./shapes.json')
.then(response => response.json())
.then(object => innerEl.color = object.shapes[0].color);
</script>
<hr>
<p>shape-container:</p>
<script>
const outerEl = document.createElement('shape-container');
document.body.appendChild(outerEl);
fetch('./shapes.json')
.then(response => response.json())
.then(object => outerEl.shapes = object.shapes);
</script>
</body>
</html>
shapes.json: allows parameterizing the shapes from an external service
{
"shapes": [
{
"color": "red"
},
{
"color": "blue"
}
]
}
shape-container.js:
import '/rect-shape.js';
(function() {
const template = document.createElement('template');
template.innerHTML = `
<style>
rect-shape {
display: inline-table;
}
</style>
`;
class ShapeContainer extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
set shapes(data) {
try {
this.shapeArray = data;
this.renderShapes();
} catch (error) {
console.error(error);
}
}
renderShapes() {
this.shapeArray.forEach(shape => {
const innerElement = document.createElement('rect-shape');
this.shadowRoot.appendChild(innerElement);
innerElement.color = shape.color;
});
}
}
customElements.define('shape-container', ShapeContainer);
}());
rect-shape.js:
(function() {
const template = document.createElement('template');
template.innerHTML = `
<style>
svg {
height: 100;
}
</style>
<div>
<svg id="port-view" viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" />
</svg>
</div>
`;
class RectShape extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
set color(value) {
if (value) {
this.setAttribute("color", value);
} else {
this.removeAttribute("color");
}
}
get color() {
return this.getAttribute("color");
}
static get observedAttributes() {
return ["color"];
}
attributeChangedCallback(name) {
switch(name) {
case "color":
this.colorizeRect();
break;
}
}
colorizeRect() {
this.shadowRoot.querySelector('rect')
.style.setProperty('fill', this.color);
}
}
customElements.define('rect-shape', RectShape);
}());
expected rendering:
rendering with chrome (sometimes):
How can I solve this problem?
Thanks,
I cleaned the code, keeping your functionality, where is Chrome going wrong?
class BaseElement extends HTMLElement {
constructor() {
super().attachShadow({mode: 'open'})
.append(document.getElementById(this.nodeName).content.cloneNode(true));
}
}
customElements.define('rect-shape', class extends BaseElement {
static get observedAttributes() {
return ["color"];
}
set color( value ) {
this.setAttribute("color", value);
}
attributeChangedCallback( name, oldValue, newValue ) {
if (name == "color") this.shadowRoot
.querySelector('rect')
.style
.setProperty('fill', newValue);
}
});
customElements.define('shape-container', class extends BaseElement {
set shapes( data ) {
data.forEach( shape => {
this.shadowRoot
.appendChild(document.createElement('rect-shape'))
.color = shape.color;
});
}
});
let shapeArray = {
"shapes": [{ "color": "red" }, { "color": "yellow" }, { "color": "blue" } ]
}
$Rect.color = 'rebeccapurple';
$Container.shapes = shapeArray.shapes;
<template id="RECT-SHAPE">
<style> svg { width: 70px }</style>
<svg viewbox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" />
</svg>
</template>
<template id="SHAPE-CONTAINER">
<style> rect-shape { display: inline-table }</style>
</template>
rect-shape:
<br><rect-shape id=$Rect></rect-shape>
<br>shape-container:
<br><shape-container id=$Container></shape-container>
I have an issue with the AnimatePresence,used for page transition.
All work properly,except the fact when i load some pages (not all pages),the content of the page don't display,even if the url is correctly pushed,and i have to manually reload the page to see the content.
Here's my _app.js:
import '../assets/css/style.css';
import { AnimatePresence, motion } from 'framer-motion';
const pageVariants = {
pageInitial: {
backgroundColor: 'black',
opacity: 0
},
pageAnimate: {
backgroundColor: 'transparent',
opacity: 1
},
pageExit: {
backgroundColor: 'black',
opacity: 0
}
}
const pageMotionProps = {
initial: 'pageInitial',
animate: 'pageAnimate',
exit: 'pageExit',
variants: pageVariants
}
function handleExitComplete() {
if (typeof window !== 'undefined') {
window.scrollTo({ top: 0 })
}
}
const App = ({ Component, pageProps, router }) => {
return (
<AnimatePresence exitBeforeEnter onExitComplete={handleExitComplete}>
<motion.div key={router.route} {...pageMotionProps}>
<Component {...pageProps}/>
</motion.div>
</AnimatePresence>
)
export default App
And here the page,created with dinamic routing and a static approach:
import ReactMarkdown from 'react-markdown'
import { getArticles, getArticle, getCategories } from '../../lib/api'
import Layout from '../../components/layout/layout'
const Article = ({ article, categories }) => {
const imageUrl = article.image.url.startsWith('/')
? process.env.API_URL + article.image.url
: article.image.url
return (
<Layout categories={categories}>
<h1>{article.title}</h1>
<div>
<ReactMarkdown source={article.content} />
</div>
</Layout>
)
}
export async function getStaticPaths() {
const articles = (await getArticles()) || []
return {
paths: articles.map((article) => ({
params: {
id: article.id,
},
})),
fallback: false,
}
}
export async function getStaticProps({ params }) {
const article = (await getArticle(params.id)) || []
const categories = (await getCategories()) || []
return {
props: { article, categories },
unstable_revalidate: 1,
}
}
export default Article
Note: Some pages work perfectly,some pages need to be reloaded to show the content,and when loaded,the animation start without problems.
I don't know if it's important,but i'm fetching data with GraphQL
I creating a comment form which would allow to post a comment below a video. It throws me id error inside the constructor that goes away on page reload. The ownProps comes out as empty object, otherwise I would've used it to get the video id. My solution with window.location.reload() is lame. Does anybody know a better one?
Comment Container
import React from 'react';
import { connect } from 'react-redux';
import Comment from './comment';
import { fetchComments, updateComment, createComment } from '../../util/comment_api_util';
const mSTP = state => {
const id = Object.keys(state.entities.videos);
const video = state.entities.videos[id];
if (video) {
return {
video,
}
} else {
window.location.reload()
}
}
const mDTP = dispatch => {
return {
fetchComments: () => dispatch(fetchComments()),
fetchComment: commentId => dispatch(fetchComment(commentId)),
createComment: comment => dispatch(createComment(comment)),
updateComment: comment => dispatch(updateComment(comment)),
deleteComment: commentId => dispatch(deleteComment(commentId))
}
}
export default connect(mSTP, mDTP)(Comment)
Comment Component
import React from 'react';
class Comment extends React.Component {
constructor(props) {
super(props)
this.state = {
body: "",
// video_id: this.props.video.id,
comment_errors: null,
}
this.update = this.update.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
update() {
return e => this.setState({ body: e.target.value })
}
handleSubmit(e) {
e.preventDefault();
const formData = new FormData();
formData.append('comment[body]', this.state.body);
formData.append('comment[video_id]', this.state.video_id);
$.ajax({
url: '/api/comments',
method: 'POST',
data: formData,
contentType: false,
processData: false
}).then(
(response) => {
this.setState(
{ comment_errors: response.responseJSON },
)
}
).then(() => {
this.setState(
{ body: "", video_id: "", comment_errors: null }
)
});
}
render() {
// if (!this.props.video) return <div />
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
<textarea
type="body"
placeholder="Add a comment"
value={this.state.body}
onChange={this.update()}
className="comment-body"/>
</label>
<button type="submit">Add comment</button>
</form>
</div>
)
}
}
export default Comment;
Play Container
import { connect } from 'react-redux';
import Play from './play';
import { fetchVideo } from '../../actions/video_actions';
import { fetchUsers } from '../../actions/user_actons';
import { fetchComments } from '../../actions/comment_actions';
const mSTP = (state, ownProps) => {
const users = Object.values(state.entities.users)
return {
video: state.entities.videos[ownProps.match.params.id],
users
}
};
const mDTP = dispatch => ({
fetchComments: () => dispatch(fetchComments()),
fetchVideo: videoId => dispatch(fetchVideo(videoId)),
fetchUsers: () => dispatch(fetchUsers()),
});
export default connect(mSTP, mDTP)(Play);
Play Component
import React from 'react';
import Comment from '../comment/comment_container'
class Play extends React.Component {
constructor(props) {
super(props);
}
dateCreated(date) {
const dateCreated = new Date(date)
return dateCreated.toLocaleDateString();
}
componentDidMount() {
this.props.fetchUsers();
this.props.fetchComments();
this.props.fetchVideo(this.props.match.params.id).then(() => {
const video = document.querySelector('.video-player');
video.muted = !video.muted;
video.play()
});
}
render() {
if (!this.props.video) { return null }
console.log(this.props)
const users = this.props.users;
const owner = users.filter(user => user.id === this.props.video.owner_id)[0]
return (
<div id="video-container">
<video
className="video-player"
controls="controls"
src={this.props.video.video_url}
autoPlay="autoplay"
muted
>
</video>
<div id="play-info">
<h1 className="play-title">{this.props.video.video_title}</h1>
<h2 className="play-date">{this.dateCreated(this.props.video.created_at)}</h2>
<h2 className="owner-name">{owner.username}</h2>
<h2 className="play-description">{this.props.video.video_description}</h2>
</div>
<Comment />
<div className="home-footer">
<h2 className="home-footer-1">#2020</h2>
<h2 className="home-footer-2">
Made with
<svg viewBox="0 0 20 20" className="_3Weix"><path d="M10 18a1.23 1.23 0 01-.8-.4 14.25 14.25 0 00-4.4-3.7C2.5 12.3 0 10.7 0 7.5a5.52 5.52 0 011.6-3.9A5.73 5.73 0 016 2a5.25 5.25 0 014 1.9A5.85 5.85 0 0114 2c2.9 0 6 2.2 6 5.5s-2.5 4.8-4.8 6.4a15.51 15.51 0 00-4.4 3.7 1.23 1.23 0 01-.8.4z" fill="rgb(255,0,0)"></path></svg>
NYC
</h2>
</div>
</div>
);
}
}
export default Play;
The only issue here is that the Comment component is trying to render before video loads. I am assuming that you are fetching video which means that if it is not there even for an instant, this error will be thrown.
The solution to this is to conditionally render Comment when you know that video exists. You can do this either in the parent or the child.
Remove this from mapStateToProps:
if (video) {
return {
video,
}
} else {
window.location.reload()
}
And check that video exists before rendering Comment
Possible solution in parent:
{video && video.id ? <Comment video={video} /> : null}
Additionally
You could render some sort of circular progress indicator instead of null
Possible solution in child
Remove video_id from state since this shouldnt be updated anyways and doesn't need to be state. Then check existence before return like this:
if (!video) return <div />
return (
// Your Comment jsx
)
Again you can render some sort of progress indicator instead of div if you want.
Why it works
Whenever video doesn't exist for that instant it will return div and wont crash. Once video exists it will re-render and now will load the Comment component instead of the div. Typically it will happen so quickly that you wont notice.
Can someone explain why I keep getting an error with the following setup:
I have this statement in my constructor:
this.bitLink = this.bitLink.bind(this),
Then I have my function as follows:
bitLink(url){
let bitly = new Bitly('f06707dhbt4c63f50d83735fa83bba16bcbdc41');
bitly.shorten(JSON.stringify(url), (response) => {
console.log(response);
}, (error) => {
console.log(error);
});
}
Then I call my function like so:
<p className="shareBtn" onClick={this.bitLink(`/album/${album._id}`)}>Share!</p>
But when I load the page I get this error:
Uncaught TypeError: Cannot read property 'bitLink' of undefined
From what I have researched, this seems to be a proper way to give this the right context but it's still returning undefined.
Edit:
Full component:
import React, { Component } from 'react'
import actions from '../actions'
import { connect } from 'react-redux'
import { APIManager } from '../utils'
import {Router, Route, Redirect, Link, withRouter } from 'react-router-dom'
import {Image, CloudinaryContext, Transformation} from 'cloudinary-react';
import Bitly from 'bitly';
class AlbumBoard extends Component {
constructor(){
super()
this.state = {
albums: []
}
}
render(){
const toPublicId = (image) => {
return image.slice(62, image.length)
}
const bitLink = (url) => {
let bitly = new Bitly('f06707da4944c63f50d83735fa83bba16bcbdc41');
bitly.shorten(JSON.stringify(url), (response) => {
console.log(response);
}, (error) => {
console.log(error);
});
}
return(
<div className="albumBoard">
{(this.props.currentUser) ?
this.props.currentUser.albums.map(function(album, i){
return <div key={i} className="thumbnailContainer">
<h2>{album.name}</h2>
<Link to={`/album/${album._id}`}>{album._id}</Link>
<p>{album.description}</p>
<div className="albumThumbnailContainer">
<CloudinaryContext cloudName="djswgrool" fetchFormat="auto">
{ (album.images.length < 3) ?
<Image publicId={toPublicId(album.images[0].url)} responsive className="album2">
<Transformation
width="200"
height="200"
crop="fill" />
</Image>
:
<div>
<Image publicId={toPublicId(album.images[0].url)} responsive className="album1">
<Transformation
width="200"
height="200"
crop="fill" />
</Image>
<Image publicId={toPublicId(album.images[1].url)} responsive className="album2">
<Transformation
width="200"
height="200"
crop="fill" />
</Image>
<Image publicId={toPublicId(album.images[2].url)} responsive className="album3">
<Transformation
width="200"
height="200"
crop="fill" />
</Image>
</div>
}
</CloudinaryContext>
</div>
<div className="X"></div>
<p className="shareBtn" onClick={bitLink(`/album/${album._id}`)}>Share!</p>
</div>
})
:
null}
</div>
)
}
}
const stateToProps = (state) => {
return {
currentUser: state.account.currentUser
}
}
export default connect(stateToProps)(AlbumBoard)
You are not passing a function reference to the onClick event but the value the returned from the function bitLink.
This is because you are invoking it
onClick={this.bitLink()}
instead of just passing the reference of it
onClick={this.bitLink}
If you want to pass a parameter to it you would need to either:
Create another wrapper function that will return and pass it the parameter:
You can do it in several ways:
currying:
bitLink(url){
return function(e){
let bitly = new Bitly('f06707dhbt4c63f50d83735fa83bba16bcbdc41');
bitly.shorten(JSON.stringify(url), (response) => {
console.log(response);
}, (error) => {
console.log(error);
});
}
}
or arrow function:
bitLink = (url) => (e){
let bitly = new Bitly('f06707dhbt4c63f50d83735fa83bba16bcbdc41');
bitly.shorten(JSON.stringify(url), (response) => {
console.log(response);
}, (error) => {
console.log(error);
});
}
or arrow function inside the event:
onClick={() => {this.bitLink(url)}}
Note that this will make a new function instance on each render.
Have you tried like this:
onClink={() => this.bitLink(/album/${album._id})}