ScrollMagic loses functionality after Gatsby build - javascript

Problem:
I tried to implement ScrollMagic via NPM to Gatsby. While during development (gatsby develop) ScrollMagic works, after gatsby build and gatsby serve ScrollMagic either shows an error or loses its functionality (both steps described below).
What I want to achieve?
Working ScrollMagic after Gatsby build.
I would be thankful for all your tips!
1. step I did: when you try build Gatsby project with ScrollMagic inside, it will show you an error:
36 | // TODO: temporary workaround for chrome's scroll jitter bug
> 37 | window.addEventListener("mousewheel", function () {});
| ^
38 |
39 | // global const
40 | var PIN_SPACER_ATTRIBUTE = "data-scrollmagic-pin-spacer";
WebpackError: ReferenceError: window is not defined
Obviously, this happens because Gatsby uses Node environment to create a build with static files and window object is not accessible in Node.
The build is therefore not finished and not successfully created. Gatsby documentation has an advise for this specific "window not defined problem" (step 2).
2. step I did:
I copied the code from Gatsby website and pasted to my gatsby-node.js:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /scrollmagic/,
use: loaders.null(),
}
],
},
})
}
}
By doing this and writing gatsby build and gatsby serve, Gatsby successfully built the web. But after I go to the subpage, where ScrollMagic should be included, it just does not work and ScrollMagic loses its functionality.
my GSAP and ScrollMagic packages:
"scrollmagic": "^2.0.5",
"gsap": "^3.2.4"
My Gatsby file where ScrollMagic should do its magic:
import React from "react"
import { graphql, Link } from "gatsby"
import SEO from "../components/SEO"
import Layout from '../layouts/Layout'
import SubpageHeader from '../components/SubpageHeader'
import SubpageItem from '../components/SubpageItem'
import styled from "styled-components"
import { gsap } from "gsap";
import ScrollMagic from 'scrollmagic';
export const query = graphql`
{
prismic {
allAbouts {
edges {
node {
short_footer_text
}
}
}
allProjectssubpages {
edges {
node {
intro_text_to_projects
ordered_projects {
link_to_project {
... on PRISMIC_Project {
_meta{
uid
}
project_title
project_description
photos {
photo
}
}
}
}
}
}
}
}
}
`
const MainContainer = styled.div`
margin: 6rem 15vw 0;
display: flex;
`
const ItemsContrainer = styled.div`
width: 45vw;
`
const LinksContainer = styled.div`
width: 25vw;
`
const LinksInnerContainer = styled.div`
display: flex;
flex-direction: column;
`
const LinkTag = styled(Link)`
text-decoration: none;
font-size: 16px;
color: #000;
margin-bottom: 15px;
letter-spacing: 2px;
opacity: 0.5;
`
class projects extends React.Component {
constructor(props) {
super(props);
}
componentDidMount = () => {
const projectssubpageData = this.props.data.prismic.allProjectssubpages.edges.slice(0, 1).pop()
const orderedProjects = projectssubpageData.node.ordered_projects;
if (typeof window !== 'undefined') {
let controller = new ScrollMagic.Controller();
const itemsContainerHeight = document.querySelector('#items-container').clientHeight;
const linksContainerHeight = document.querySelector('#links-inner-container').clientHeight;
const height = itemsContainerHeight - linksContainerHeight;
let scene = new ScrollMagic.Scene({ triggerElement: "#main-container", triggerHook: 0.25, duration: height })
.setPin("#links-container")
.addTo(controller);
orderedProjects.forEach(project => {
let uidOfProject = project.link_to_project._meta.uid;
let projectDivHeight = document.querySelector(`.${uidOfProject}`).clientHeight;
let scene = new ScrollMagic.Scene({ triggerElement: `.${uidOfProject}`, triggerHook: 0.4, duration: projectDivHeight })
.setClassToggle(`#${uidOfProject}`, "active")
.addTo(controller);
});
}
}
render() {
const footerData = this.props.data.prismic.allAbouts.edges.slice(0, 1).pop()
const projectssubpageData = this.props.data.prismic.allProjectssubpages.edges.slice(0, 1).pop()
if (!projectssubpageData || !footerData) return null
return (
<>
<SEO title="Projects" />
<Layout
footerShortText={footerData.node.short_footer_text[0].text}
footerLinksArray={[
{
name: 'All Events',
URL: '/events'
},
{
name: 'Diary',
URL: '/diary'
},
{
name: 'Home',
URL: '/'
}
]}>
{/* Subpage header */}
<SubpageHeader headline={"PROJECTS"} introText={projectssubpageData.node.intro_text_to_projects[0].text}></SubpageHeader>
<MainContainer id="main-container">
{/* Links to the projects */}
<LinksContainer id="links-container">
<LinksInnerContainer id="links-inner-container">
{projectssubpageData.node.ordered_projects.map(project => (
<LinkTag
to={"projects/" + project.link_to_project._meta.uid}
key={project.link_to_project._meta.uid}
id={project.link_to_project._meta.uid}>
{project.link_to_project.project_title[0].text}
</LinkTag>
))}
</LinksInnerContainer>
</LinksContainer>
{/* All projects */}
<ItemsContrainer id="items-container">
{projectssubpageData.node.ordered_projects.map(project => (
<SubpageItem
itemURL={"projects/" + project.link_to_project._meta.uid}
photoURL={project.link_to_project.photos[0].photo.url}
photoAlt={project.link_to_project.photos[0].photo.alt}
title={project.link_to_project.project_title[0].text}
description={project.link_to_project.project_description[0].text}
divClass={project.link_to_project._meta.uid}
key={project.link_to_project._meta.uid}>
</SubpageItem>
))}
</ItemsContrainer>
</MainContainer>
</Layout>
</>
)
}
}
export default projects

I did one workaround which seems to work so I am posting it here. Instead of implementing scrollmagic at the beginning of the document, I implement it later after componentDidMount is called and after I can be 100% sure that the window object exists.
here you go:
componentDidMount = () => {
if (typeof window !== 'undefined') {
import('scrollmagic').then((ScrollMagic) => {
// your scrollmagic code here
let controller = new ScrollMagic.Controller();
// ....
});
}
}
This way the error disappears and therefore step 2 (step 2 from question) is not needed.

Related

Switch between CSS variables based on states

I'm trying to implement color-themes into my react app and went with css-variables and the basics work out so far.
However I'm also trying to apply the theme to an SVG icon that changes the color based on a state. It goes from grey to a color which is defined by the theme when a phone gets connected to a device.
I have a themes.css which looks like this:
:root {
--fillInactive: #7c7c7c;
}
[data-theme='Blue'] {
--fillHover: #708fa8;
--fillActive: #3f77a4;
}
[data-theme='Red'] {
--fillHover: #cd6969;
--fillActive: #cd2424;
}
[data-theme='Green'] {
--fillHover: #85b08e;
--fillActive: #41ad56;
}
[data-theme='White'] {
--fillHover: #9f9f9f;
--fillActive: #ffffff;
}
My topbar.js looks like this:
import React from "react";
import { useState, useEffect } from "react";
import "./topbar.scss";
import "../components/themes.scss"
const electron = window.require('electron');
const { ipcRenderer } = electron;
const TopBar = () => {
const Store = window.require('electron-store');
const store = new Store();
const [phoneState, setPhoneState] = useState("#7c7c7c");
const [theme, setTheme] = useState(store.get("colorTheme"));
const plugged = () => {
setPhoneState("#3f77a4");
}
const unplugged = () => {
setPhoneState("#7c7c7c");
}
useEffect(() => {
ipcRenderer.send('statusReq');
ipcRenderer.on("plugged", plugged);
ipcRenderer.on("unplugged", unplugged);
return function cleanup() {
ipcRenderer.removeListener('plugged', plugged);
ipcRenderer.removeListener('unplugged', unplugged);
};
}, []);
return (
<div className="topbar" data-theme={theme}}>
<div className="topbar__info">
<svg className="topbar__icon">
<use xlinkHref="./svg/phone.svg#phone" color={phoneState}></use>
</svg>
</div>
</div>
);
};
export default TopBar;
My TopBar.scss looks like this:
.topbar {
display: flex;
flex-direction: row;
justify-content: space-between;
position: absolute;
top: 0;
height: 66px;
width: 100%;
background: #000000;
&__info {
margin-left: 3rem;
}
&__icon {
&__phone {
}
margin-top: 1rem;
margin-right:1rem;
width: 1rem;
height: 1rem;
}
}
For now I hardcoded a blue color into my JS, but would like that the property "--fillActive" is set for the SVG when the phone is plugged, based on the chosen Theme.
When the phone is unplugged it should switch to "--fillInactive" so it greys out again.
I hope my question is specific enough. I tried googling for a solution, but I'm not even sure that what I'm trying to do is the right way, hence trying to find a good search query is rather difficult. I saw some solutions where you can just override a variable through javascript, but I would like that the properties stay as they are and that I just choose between the two to set the correct color based on the state.
Any help or hint is highly appreciated.
As an idea, the phoneState could contain the actual phone state: plugged/unplugged:
const [phoneState, setPhoneState] = useState(() => 'unplugged');
Then the corresponding callback would toggle the state:
useEffect(() => {
ipcRenderer.send('statusReq');
ipcRenderer.on("plugged", setPhoneState(() => 'plugged'));
ipcRenderer.on("unplugged", setPhoneState(() => 'unplugged'));
return function cleanup() { ... },
[]);
In this case the phoneState can be used for a dynamic class of some element, for example:
return (
<div className="topbar" data-theme={theme}}>
<div className={`topbar__info topbar__info--${phoneState}`}>
<svg className="topbar__icon">
<use xlinkHref="./svg/phone.svg#phone" ></use>
</svg>
</div>
</div>
);
Then when the css vars are applied, they would have corresponding values according to the active theme.
.topbar {
&__info{
&--plugged{
color: var(--fillActive);
}
&--unplugged{
color: var(--fillInactive);
}
}
}

MUI DataGridPro crashes when reordering columns by drag&drop, when already using react-dnd in the parent component

My app is a dashboard of MUI <Card />s that can be dragged-and-dropped (d&d) to reorder them. The d&d logic is implemented using react-dnd and has been working well so far.
However, when I add a <DataGridPro /> as the child of a draggable <Card />, the datagrid's native Column ordering - which also is done by dragging-and-dropping - breaks. Dragging a column once or twice generates the following crash:
Invariant Violation
Cannot call hover while not dragging.
▼ 5 stack frames were expanded.
at invariant (https://bfz133.csb.app/node_modules/
react-dnd/invariant/dist/index.js:19:15
checkInvariants
https://bfz133.csb.app/node_modules/dnd-core/dist/actions/dragDrop/hover.js:33:40
DragDropManagerImpl.hover
https://bfz133.csb.app/node_modules/dnd-core/dist/actions/dragDrop/hover.js:18:5
Object.eval [as hover]
https://bfz133.csb.app/node_modules/dnd-core/dist/classes/DragDropManagerImpl.js:25:38
HTML5BackendImpl.handleTopDrop
https://bfz133.csb.app/node_modules/react-dnd-html5-backend/dist/HTML5BackendImpl.js:455:20
▲ 5 stack frames were expanded.
This screen is visible only in development. It will not appear if the app crashes in production.
Open your browser’s developer console to further inspect this error.
This error overlay is powered by `react-error-overlay` used in `create-react-app`.
You can begin dragging, the crash only happens when you let go of the mouse button.
The expected behavior is that I should be able to d&d the columns, to change their order, without issues.
Things I've tried
Removing the <DataGridPro /> and replacing that <Card /> with a text-type Card (see the code in the sandbox below) shows that d&d logic works fine with no crashes;
Disabling my app's d&d by commenting out all the relevant code causes the <DataGridPro />'s colum reordering to work as expected;
The above suggests the root cause lies in having both D&Ds work without causing an internal conflict in react-dnd, which led to me trying:
Browsing the documentation to find a way to instruct the component to use my own DndProvider or DndManager, but I couldn't find that in the API - sorry if I misread it!
Googling for the error message "Cannot call hover while not dragging", while limiting myself to contexts including the MUI library or react-dnd, yielded limited results. I found a Chrome bug that was fixed on v. 77.0.3865.120, my Chrome version is 101.0.4951.64 .
EDIT: Found this bug, but it's closed. Should I open a new one? I'd like some input on this, as I wouldn't like to bother the developers if the problem is in my code.
Minimum verified reproducible example
I made a sandbox! Click here to see it
Datagrid Component:
import React from "react";
import { DataGridPro } from "#mui/x-data-grid-pro";
import { useDemoData } from "#mui/x-data-grid-generator";
export function MyDatagridPro() {
const { data } = useDemoData({
dataSet: "Commodity",
rowLength: 5,
maxColumns: 6
});
return <DataGridPro {...data} />;
}
Card widget:
import React, { useRef } from "react";
import {
Card,
CardHeader,
CardContent,
Grid,
Typography,
Divider
} from "#mui/material";
import { useDrag, useDrop } from "react-dnd";
import { MyDatagridPro } from "./MyDatagridPro";
export function MyContentCard(props) {
const domRef = useRef(null);
const [{ isDragging }, dragBinder, previewBinder] = useDrag(
() => ({
type: "mycard",
item: () => ({
orderIndex: props.orderIndex
}),
collect: (monitor) => ({
isDragging: monitor.isDragging()
})
}),
[props]
);
const [{ handlerId, isOver }, dropBinder] = useDrop(
() => ({
accept: "mycard",
collect: (monitor) => ({
handlerId: monitor.getHandlerId(),
isOver: !!monitor.isOver()
}),
canDrop: (item, monitor) => {
if (!domRef.current) return false;
const draggingOrderIndex = item.orderIndex;
const hoveringOrderIndex = props.orderIndex;
if (draggingOrderIndex === hoveringOrderIndex) return false;
const hoverRectangleBound = domRef.current?.getBoundingClientRect();
const [hoverItemX, hoverItemY] = [
hoverRectangleBound.right - hoverRectangleBound.left,
hoverRectangleBound.bottom - hoverRectangleBound.top
];
const mousePosition = monitor.getClientOffset();
const [hoverMouseX, hoverMouseY] = [
mousePosition.x - hoverRectangleBound.left,
mousePosition.y - hoverRectangleBound.top
];
if (
(hoverMouseX < 0 || hoverMouseX > hoverItemX) &&
(hoverMouseY < 0 || hoverMouseY > hoverItemY)
) {
return false;
}
return true;
},
drop: (item) => {
props.swapper(item.orderIndex, props.orderIndex);
}
}),
[props]
);
return (
<Grid item xs={5}>
<Card
ref={(element) => {
if (element) {
domRef.current = element;
previewBinder(dropBinder(domRef));
}
}}
sx={{
height: `calc(6 * 4.5rem)`,
opacity: isDragging ? 0.3 : 1,
display: "flex",
flexDirection: "column",
border: isOver ? "2px solid rgba(0,0,0,0.5);" : ""
}}
data-handler-id={handlerId}
>
<CardHeader ref={dragBinder} title={props.title} />
<Divider />
<CardContent
sx={{
height: "100%",
display: "flex",
flexDirection: "column"
}}
>
{props.type === "text" && <Typography>{props.content}</Typography>}
{props.type === "datagrid" && <MyDatagridPro />}
</CardContent>
</Card>
</Grid>
);
}
export default MyContentCard;
App.js:
import React, { useState } from "react";
import { Grid } from "#mui/material";
import { createDragDropManager } from "dnd-core";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { MyContentCard } from "./MyContentCard";
export const dndManager = createDragDropManager(HTML5Backend);
export default function App() {
const [cards, setCards] = useState([
{
type: "datagrid",
title: "Card 01 - A MUI DataGridPro",
content: ""
},
{
type: "text",
title: "Card 02 - Some text",
content: "Text that belongs to card 2"
}
]);
function swapCards(indexA, indexB) {
const newState = cards.slice();
const cardA = Object.assign({}, cards[indexA]);
newState[indexA] = Object.assign({}, cards[indexB]);
newState[indexB] = cardA;
setCards(newState);
}
return (
<DndProvider manager={dndManager}>
<Grid
container
spacing={1}
columns={10}
p={2}
pb={3}
mt={0}
mb={0}
// flex="1 1 auto"
overflow="auto"
sx={{
backgroundColor: "lightgray"
}}
>
{cards.map((card, i) => {
return (
<MyContentCard
key={i}
type={card.type}
title={card.title}
content={card.content}
orderIndex={i}
swapper={swapCards}
/>
);
})}
</Grid>
</DndProvider>
);
}

Vue3 Web Components events not working with custom wrapper

The WebComponents do work fine with this custom script but I have problems to.
I want to be able to add event listener from outside to listen to the custom Vue events fired from within the component (in this example on click on the WC). Thus I created an event proxy but for some reason the listener is never triggered. Obviously I made a mistake somewhere. So I hope that someone can point me to the right solution.
I made a CodeSandbox for you to take a look and play around with the code.
(as CodeSandbox has some issues you'll need to click on the reload button on the right side of the layout in order to make it work)
//main.js
import { createApp } from "vue";
import App from "./App.vue";
import WebComponentService from "./services/wc";
WebComponentService.init();
createApp(App).mount("#app");
Vue Component used as source file for Web Component
//src/wcs/MyComp.vue
<template>
<div class="m" #click="click">
Hello My Comp {{ name }}
<div v-for="(i, index) in values" :key="index">ID: {{ i.title }}</div>
</div>
</template>
<script>
export default {
__useShadowDom: false,
emits: ["my-click"],
// emits: ["my-click", "myclick"],
props: {
name: {
default: "Test",
},
values: {
type: Object,
default: () => {
return [{ title: "A" }, { title: "B" }];
},
},
},
methods: {
click() {
// emit the event
this.$emit("my-click");
console.log("EMIT");
},
},
};
</script>
<style lang="less" scoped>
.m {
border: 5px solid red;
margin: 10px;
padding: 10px;
border-radius: 5px;
}
</style>
Index.html where I test the web component and added an event listener
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<h3>WC Test here</h3>
<div class="component-canvas">
<!-- web component testing -->
<xvue-my-comp
id="vtest"
name="MyComp HTML Rendering"
:values='[{"title": "Element 1"}, {"title": "Element"}]'
>Inner HTML Caption</xvue-my-comp
>
</div>
<script>
const mySelector = document.querySelector("xvue-my-comp");
//listen to the event
mySelector.addEventListener("my-click", function (ev) {
console.log("#LISTEN");
alert(ev);
});
</script>
</body>
</html>
Custom WebComponents wrapper
import HTMLParsedElement from "html-parsed-element";
import { createApp, h, toHandlerKey } from "vue";
import { snakeCase, camelCase } from "lodash";
let registeredComponents = {};
const tagPrefix = "xvue-"; // Prefix for Custom Elements (HTML5)
const vueTagPrefix = "xvue-init-"; // Prefix for VUE Components - Must not be tagPrefix to avoid loops
// We use only one file as web component for simplicity
// and because codesandbox doesen't work with dynamic requires
const fileName = "MyComp.vue";
const webComponent = require(`../wcs/MyComp.vue`);
const componentName = snakeCase(
camelCase(
// Gets the file name regardless of folder depth
fileName
.split("/")
.pop()
.replace(/.ce/, "")
.replace(/\.\w+$/, "")
)
).replace(/_/, "-");
// store our component
registeredComponents[componentName] = {
component: webComponent.default
};
export default {
init() {
// HTMLParsedElement is a Polyfil Library (NPM Package) to give a clean parsedCallback as the browser
// default implementation has no defined callback when all props and innerHTML is available
class VueCustomElement extends HTMLParsedElement {
// eslint-disable-next-line
constructor() {
super();
}
parsedCallback() {
console.log("Legacy Component Init", this);
if (!this.getAttribute("_innerHTML")) {
this.setAttribute("data-initialized", this.innerHTML);
}
let rootNode = this;
let vueTagName = this.tagName
.toLowerCase()
.replace(tagPrefix, vueTagPrefix);
let compBaseName = this.tagName.toLowerCase().replace(tagPrefix, "");
let compConfig = registeredComponents[compBaseName];
// Optional: Shadow DOM Mode. Can be used by settings __useShadowDom in component vue file
if (compConfig.component.__useShadowDom) {
rootNode = this.attachShadow({ mode: "open" });
document
.querySelectorAll('head link[rel=stylesheet][href*="core."]')
.forEach((el) => {
rootNode.appendChild(el.cloneNode(true));
});
}
if (vueTagName) {
// If we have no type we do nothing
let appNode = document.createElement("div");
// let appNode = rootNode;
appNode.innerHTML +=
"<" +
vueTagName +
">" +
this.getAttribute("data-initialized") +
"</" +
vueTagName +
">"; // #TODO: Some issues with multiple objects being created via innerHTML and slots
rootNode.appendChild(appNode);
// eslint-disable-next-line #typescript-eslint/no-this-alias
const self = this;
function createCustomEvent(name, args) {
return new CustomEvent(name, {
bubbles: false,
cancelable: false,
detail: args.length === 1 ? args[0] : args
});
}
const createEventProxies = (eventNames) => {
const eventProxies = {};
if (eventNames) {
console.log("eventNames", eventNames);
// const handlerName = toHandlerKey(camelCase(name));
eventNames.forEach((name) => {
const handlerName = name;
eventProxies[handlerName] = (...args) => {
this.dispatchEvent(createCustomEvent(name, args));
};
});
}
return eventProxies;
};
const eventProxies = createEventProxies(compConfig.component.emits);
this._props = {};
const app = createApp({
render() {
let props = Object.assign({}, self._props, eventProxies);
// save our attributes as props
[...self.attributes].forEach((attr) => {
let newAttr = {};
newAttr[attr.nodeName] = attr.nodeValue;
props = Object.assign({}, props, newAttr);
});
console.log("props", props);
delete props.dataVApp;
return h(compConfig.component, props);
},
beforeCreate: function () {}
});
// Append only relevant VUE components for this tag
app.component(vueTagPrefix + compBaseName, compConfig.component);
this.vueObject = app.mount(appNode);
console.log("appNode", app.config);
}
}
disconnectedCallback() {
if (this.vueObject) {
this.vueObject.$destroy(); // Remove VUE Object
}
}
adoptedCallback() {
//console.log('Custom square element moved to new page.');
}
}
// Register for all available component tags ---------------------------
// Helper to Copy Classes as customElement.define requires separate Constructors
function cloneClass(parent) {
return class extends parent {};
}
for (const [name, component] of Object.entries(registeredComponents)) {
customElements.define(tagPrefix + name, cloneClass(VueCustomElement));
}
}
};
If I whack this into your render method, it all works fine.
Thus your problem is not Web Components or Events related
document.addEventListener("foo", (evt) => {
console.log("FOOd", evt.detail, evt.composedPath());
});
self.dispatchEvent(
new CustomEvent("foo", {
bubbles: true,
composed: true,
cancelable: false,
detail: self
})
);
addendum after comment:
Your code has bubbles:false - that will never work
This code properly emits an Event
You need to look into how you trigger your proxy code
const createCustomEvent = (name, args = []) => {
return new CustomEvent(name, {
bubbles: true,
composed: true,
cancelable: false,
detail: !args.length ? self : args.length === 1 ? args[0] : args
});
};
const createEventProxies = (eventNames) => {
const eventProxies = {};
if (eventNames) {
eventNames.forEach((name) => {
const handlerName =
"on" + name[0].toUpperCase() + name.substr(1).toLowerCase();
eventProxies[handlerName] = (...args) => {
appNode.dispatchEvent(createCustomEvent(name));
};
});
document.addEventListener("foo",evt=>console.warn("foo!",evt));
appNode.dispatchEvent(createCustomEvent("foo"));
console.log(
"#eventProxies",
eventProxies,
self,
"appnode:",
appNode,
"rootNode",
rootNode
);
}
return eventProxies;
};
I guess you could try to use Vue3's (since 3.2) defineCustomElement
import CustomElement from './some/path/CustomElement.ce.vue'
const CustomElementCE = defineCustomElement(CustomElement)
customElements.define('custom-element', CustomElementCE);
More on that here: Vue web components
I found one possible solution. As stated in the vue docs we can use static event listeners that are passed as props. They have to be prefixed with "on" i.E. onMyevent for event name my-event. Though it works fine - the downside is that I can't pass any arguments as I don't see where to get them from.
One strange thing is that I cant add this event to the instance. Something like self.dispatchEvent() doesn't work when placed inside createEventProxies method. So I have to stick to document.dispatchEvent()
Working CodeSandbox.
const createCustomEvent = (name, args = []) => {
return new CustomEvent(name, {
bubbles: false,
composed: true,
cancelable: false,
detail: !args.length ? self : args.length === 1 ? args[0] : args
});
};
const createEventProxies = (eventNames) => {
const eventProxies = {};
if (eventNames) {
eventNames.forEach((name) => {
const handlerName =
"on" + name[0].toUpperCase() + name.substr(1).toLowerCase();
eventProxies[handlerName] = (...args) => {
document.dispatchEvent(createCustomEvent(name));
};
});
console.log("#eventProxies", eventProxies);
}
return eventProxies;
};

Best way to add single component transitions on page change to nuxt components?

I've tested with CSS/SCSS so far, it seems to work only on first page change :
.page-leave-active {
.news-item {
#for $i from 1 through 10 {
transition-delay: $i * 300ms;
}
transform: translateX(115%);
}
}
I was wondering is there a way to add JS transitions inside a component? Is there an equivalent to mounted, but for unmount?
if you don't use dynamic page transition name for ssr-nuxt page transition, just add the code in transition attribute like examples in nuxtjs document
if you are using a dynamic page transition name, it's a little hard if you use transition attribute that nuxt provided. I use below code for dynamic page transition name, and it works well for me(don't forget code your own <error-view/> component)
use this instead of <nuxt/>, you can add your logic easily in methods
<template>
<main>
<transition
mode="out-in"
:name="transitionName"
#beforeLeave="beforeLeave"
#enter="enter"
#afterEnter="afterEnter">
<router-view v-if="!nuxt.err"/>
<error-view :error="nuxt.err" v-else/>
</transition>
</main>
</template>
<script type="text/javascript">
import Vue from 'vue';
import ErrorView from '#/layouts/error';
export default{
components: {
ErrorView
},
data() {
return {
prevHeight: 0,
transitionName: 'fade'
};
},
beforeCreate () {
Vue.util.defineReactive(this, 'nuxt', this.$root.$options.nuxt);
},
created() {
this.$router.beforeEach((to, from, next) => {
let transitionName = to.meta.transitionName || from.meta.transitionName;
if (transitionName === 'slide') {
const toDepth = to.path.split('/').length;
const fromDepth = from.path.split('/').length;
transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left';
}
this.transitionName = transitionName || 'fade';
next();
});
},
methods: {
beforeLeave(el) {
this.prevHeight = getComputedStyle(el).height;
},
enter(el) {
const { height } = getComputedStyle(el);
el.style.height = this.prevHeight;
setTimeout(() => {
el.style.height = height;
}, 0);
},
afterEnter(el) {
el.style.height = 'auto';
}
}
}
</script>
Here is what you get
Full resource code of the demo that gif picture shows is here

Intersection Observer Scrolling Problem On Mobile Devices

I'm having trouble with Intersection Observer in Gatsbyjs.
There is a fixed header that needs to change the color when some section enters 90% of the viewport.
I've created a component called IntersectionObserverComponent that renders a div with absolute position and height of 100vh. The idea is to include this component into a section that I want to track with Intersection Observer.
Here's my component
class IntersectionObserverComponent extends React.PureComponent {
constructor(props) {
super(props)
//
this.observer = null
this.ref = React.createRef()
}
componentDidMount() {
const { root, threshold, callback } = this.props
('IntersectionObserver' in window
? Promise.resolve()
: import('intersection-observer')
).then(() => {
this.observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
callback(entry)
})
},
{ threshold, root }
)
if (this.ref.current) {
this.observer.POLL_INTERVAL = 100
this.observer.observe(this.ref.current)
}
})
}
componentWillUnmount() {
if (this.ref.current && this.observer) {
this.observer.unobserve(this.ref.current)
}
}
render() {
return (
<div
ref={this.ref}
style={{
height: '100vh',
position: 'absolute',
top: 0,
left: 0,
right: 0,
}}
></div>
)
}
}
export default IntersectionObserverComponent
And here's the usage
import { handleHeaderStyle } from '../../utils/intersectionObserver'
const About = ({ isTrackable = false, handleChangeHeader = () => {} }) => {
// previousValues object is directly mutated in a handleHeaderStyle fn
const previousValues = { previousY: 0, previousRatio: 0 }
const colors = { prevColor: 'white', currColor: 'secondary' }
const handleIOCallback = entry => {
console.log(entry)
handleHeaderStyle(entry, colors, previousValues, handleChangeHeader)
}
return (
<section className="padding-top-m padding-bottom-tablet-portrait-96 header-behind background-gray-small-only">
{isTrackable ? <IO callback={handleIOCallback} /> : null}
<Container>
<Grid className="grid-padding-x">
<Cell small={12} large={8}>
<p className="margin-bottom-small-20 margin-bottom-tablet-portrait-32">
We are a brand and product development consultancy, that believes
in that design is essentially a problem-solving exercise that we
can apply to any sector, discipline or media.
</p>
<p className="margin-bottom-small-32">
We are founded on and by people who are driven by:
</p>
<ul className="arrow-bullets">
<li>
Crafting simple and notable components that build up your
brand’s value
</li>
<li>Discovery and research aimed at thoughtful practices</li>
<li>Attention to detail</li>
<li>Independence and creative freedom</li>
<li>Thinking that goes deeper</li>
</ul>
</Cell>
<Cell className="padding-top-m">
<Projects />
</Cell>
</Grid>
</Container>
</section>
)
}
export default About
And here's the function that handles logic for updating header
export const handleHeaderStyle = (
IOEntry,
colors,
previousValues,
callback
) => {
if (!IOEntry || !colors || !previousValues || typeof callback !== 'function')
return
//
const { prevColor, currColor } = colors
const currentY = IOEntry.boundingClientRect.y
const currentRatio = IOEntry.intersectionRatio
const isIntersecting = IOEntry.isIntersecting
// going up
if (currentY > previousValues.previousY) {
if (currentRatio < previousValues.previousRatio && !isIntersecting) {
callback(prevColor)
}
}
// going down
else if (currentY < previousValues.previousY && isIntersecting) {
if (currentRatio > previousValues.previousRatio) {
callback(currColor)
}
}
previousValues.previousY = currentY
previousValues.previousRatio = currentRatio
}
This is working really nice on all screen sizes when I test it in a desktop browser but the problem occurs when I test it on an actual mobile device.
Device: Iphone 7+, IOS 13.3.1
Browsers: Safari 12.1.1, Chrome 80
It works great if I don't release my finger while scrolling, but If I do, the callback is not fired consistently. I've also tried setting the POLL_INTERVAL to 100ms but it didn't help.
I'm suspecting that has something to do with the way iOS Safari handles the scroll event (https://github.com/martinlaxenaire/curtainsjs/issues/18)
Here's the test link on Netlify - https://nifty-einstein-c4cf08.netlify.com/
I'm trying to figure it out for days now, thanks in advance

Categories

Resources