Add a class to every child of a slot - javascript

I'm trying to set up a component with a slot, that when rendered adds a class to every children of that slot. In a very simplified manner:
<template>
<div>
<slot name="footerItems"></slot>
</div>
</template>
How would I go about this? My current solution is to add the class to the elements in an onBeforeUpdate hook:
<script setup lang="ts">
import { useSlots, onMounted, onBeforeUpdate } from 'vue';
onBeforeUpdate(() => addClassToFooterItems());
onMounted(() => addClassToFooterItems());
function addClassToFooterItems() {
const slots = useSlots();
if (slots && slots.footerItems) {
for (const item of slots.footerItems()) {
item.el?.classList.add("card-footer-item");
}
}
}
</script>
However, the elements lose the styling whenever it's rerendered (using npm run serve) and also jest tests give me a warning:
[Vue warn]: Slot "footerItems" invoked outside of the render function: this will not track dependencies used in the slot. Invoke the slot function inside the render function instead.
Should I move the slot to its own component and use a render function there? But even then I'm not sure how to edit the children to add the classes or how to produce several root level elements from the render function.

So, I managed to solve this in an incredibly hacky way, but at least my issue with re-rendering doesn't happen anymore and jest doesn't complain. I wrote a component with a render function that appends that class to all children
<template>
<render>
<slot></slot>
</render>
</template>
<script setup lang="ts">
import { useSlots } from 'vue';
const props = defineProps<{
childrenClass: string;
}>();
function recurseIntoFragments(element: any): any {
if (element.type.toString() === 'Symbol(Fragment)'
&& element.children[0].type.toString() === 'Symbol(Fragment)'
) {
return recurseIntoFragments(element.children[0]);
} else {
return element;
}
}
const render = () => {
const slot = useSlots().default!();
recurseIntoFragments(slot[0]).children.forEach((element: any) => {
if (element.props?.class && !element.props?.class.includes(props.childrenClass)) {
element.props.class += ` ${props.childrenClass}`;
} else {
element.props.class = props.childrenClass;
}
});
return slot;
}
</script>
Then I would just wrap the slot in this component to add the class to the children elements:
<template>
<div>
<classed-slot childrenClass="card-footer-item">
<slot name="footerItems"></slot>
</classed-slot>
</div>
</template>
I would gladly accept any answer that improves upon this solution, especially:
Any tips to type it. All those anys feel wonky but I find it very impractical working with Vue types for slots since they are usually unions of 3 or 4 types and the only solution is to wrap these in type checks
Anything that improves its reliability since it seems that it'd crash in any slightly different setup than the one I intended
Any recommendation based on Vue's (or TS) best practices, since this looks very amateurish.
Really any other way to test for symbol equality because I know none
EDIT
This is my latest attempt, a render function in a file ClassedSlot.js:
import { cloneVNode } from 'vue';
function recursivelyAddClass(element, classToAdd) {
if (Array.isArray(element)) {
return element.map(el => recursivelyAddClass(el, classToAdd));
} else if (element.type.toString() === 'Symbol(Fragment)') {
const clone = cloneVNode(element);
clone.children = recursivelyAddClass(element.children, classToAdd)
return clone;
} else {
return cloneVNode(element, { class: classToAdd });
}
}
export default {
props: {
childrenClass: {
type: String,
required: true
},
},
render() {
const slot = this.$slots.default();
return recursivelyAddClass(slot, this.$props.childrenClass);
},
};
The usage of this component is exactly the same as in the previous one. I'm kinda happy with this solution, seems more robust and idiomatic. Note that it's javascript because I found it really hard to type these functions correctly.

#Haf 's answer is good.
I made some adjustments, so that you can specify the component name.
import { FunctionalComponent, StyleValue, cloneVNode, Ref, ComputedRef, unref } from "vue";
type MaybeRef<T> = T | Ref<T>;
/**
* #param extraProps - ref or normal object
* #returns
*
* #example
*
* const StyleComponent = useCssInJs({class: 'text-red-500'});
*
* const ComponentA = () => {
* return <StyleComponent><span>text is red-500</span></StyleComponent>
* }
*/
export const useCssInJs = (extraProps: MaybeRef<{
class?: string,
style?: StyleValue
}>) => {
const FnComponent: FunctionalComponent = (_, { slots }) => {
const defaultSlot = slots.default?.()[0];
// could be Fragment or others? I'm just ignoring these case here
if (!defaultSlot) return;
const node = cloneVNode(defaultSlot, unref(extraProps));
return [node];
};
return FnComponent
}

Related

How do you access react hooks in multiple components? What am I doing wrong with my react hook?

Im trying to pass a state value into a component. Why is it working in one component and not working in another component in the same folder?
I have the hooks in here. Im trying to access "currentGuess". In this function I initialize the state of currentGuess to "", then the next part just sets the "currentGuess" to whatever you type in.
----------------------/src/hooks/useWordle.js----------------------
const useWordle = (solution) => {
const [currentGuess, setCurrentGuess] = useState("");
/* OTHER UNNECESSARY CODE TO QUESTION */
const handleInput = ({ key }) => {
if (key === "Enter") {
if (turn > 5) {
console.log("You used all your guesses!");
return;
}
if (history.includes(currentGuess)) {
console.log("You already tried that word!");
return;
}
if (currentGuess.length !== 5) {
console.log("Word must be 5 characters long!");
return;
}
const formatted = formatGuessWord();
console.log(formatted);
}
if (key === "Backspace") {
setCurrentGuess((state) => {
return state.slice(0, -1);
});
}
if (/^[a-zA-z]$/.test(key))
if (currentGuess.length < 5) {
setCurrentGuess((state) => {
return state + key;
});
}
};
return {
currentGuess,
handleInput,
};
};
export default useWordle;
I can use it in here like this and it works no problem:
----------------------src/components/Wordle.js----------------------
import React, { useEffect } from "react";
import useWordle from "../hooks/wordleHooks.js";
function Wordle({ solution }) {
const { currentGuess, handleInput } = useWordle(solution);
console.log("currentGuess=", currentGuess);
useEffect(() => {
window.addEventListener("keyup", handleInput);
return () => window.removeEventListener("keyup", handleInput);
});
return <div>Current guess: {currentGuess}</div>;
}
export default Wordle;
I thought this line was what allowed me to use "currentGuess". I destructured it.
const { currentGuess, handleInput } = useWordle(solution);
However when I place that line in this code, "currentGuess" comes out undefined or empty.
----------------------/src/components/Key.js----------------------
import React, { useContext } from "react";
import { AppContext } from "../App";
import useWordle from "../hooks/wordleHooks.js";
export default function Key({ keyVal, largeKey }) {
const { onSelectLetter, onDeleteKeyPress, onEnterKeyPress } =
useContext(AppContext);
const { currentGuess } = useWordle();
const handleTypingInput = () => {
console.log("Key.js - Key() - handleTypingInput() - {currentGuess}= ", {
currentGuess,
}); // COMES OUT "Object { currentGuess: "" }"
};
If you made it this far thank you very much. Im new to this and hoping someone who knows what they are doing can see some glaring flaw in the code I can fix. You don't even have to solve it for me but can you point me in the right direction? How do I make the "currentGuess" variable/state be accessible in the Key.js component?
Hooks are meant to share behavior, not state. The call to useWordle in your Wordle component creates a completely different variable in memory for currentGuess than the call to useWordle in the Key component (remember, after all, that hooks are just regular old functions!). In other words, you have two completely separate versions of currentGuess floating around, one in each component.
If you'd like to share state, use the Context API, use a state management library (such Redux, MobX, etc.), or just lift state up.

Rerender only specific Child Components in JSX map function

I am mapping through an array, which returns JSX Components for each of the items in the array. During runtime I want to pass down values. If they match the value of the individual items, their individual component gets modified.
I am trying to find a way to achieve this without rerendering all components, which currently happens because the props change
I have tried using shouldComponentUpdate in a class component, but it seems this way I can only compare prevState and prevProps with the corresponding changes. I have further considered useMemo in the Map function, which didnt work, because it was nested inside the map function.
const toParent=[1,2,4,5]
Parent Component:
function parent({ toParent }) {
const [myNumbers] = useState([1,2,3,4, ..., 1000]);
return (
<div>
{myNumbers.map((number, index) => (
<Child toChild = { toParent } number = { number }
index= { index } key = { number }/>
))}
</div>
)
}
Child Component:
function Child({toChild, number, index}){
const [result, setResult] = useState(() => { return number*index }
useEffect(()=> {
if (toChild.includes(number)) {
let offset = 10
setResult((prev)=> { return { prev+offset }})
}
}, [toChild])
return (
<div style={{width: result}}> Generic Div </div> )
}
The solution to my problem was using the React.memo HOC and comparing the properties to one another and exporting it as React.memo(Child, propsAreEqual).
Performance
This way other methods like findElementbyId (not recommended in any case) and shouldComponentUpdate to target specific items in a map function can be avoided.
Performance is quite good, too. Using this method cut down the rendering time from 40ms every 250ms to about 2 ms.
Implementation
In Child Component:
function Child(){...}
function propsAreEqual(prev, next) {
//returning false will update component, note here that nextKey.number never changes.
//It is only constantly passed by props
return !next.toChild.includes(next.number)
}
export default React.memo(Child, propsAreEqual);
or alternatively, if other statements should be checked as well:
function Child(){...}
function propsAreEqual(prev, next) {
if (next.toChild.includes(next.number)) { return false }
else if ( next.anotherProperty === next.someStaticProperty ) { return false }
else { return true }
}
export default React.memo(Key, propsAreEqual);

Passing data up through nested Components in React

Prefacing this with a thought; I think I might require a recursive component but that's beyond my current ability with native js and React so I feel like I have Swiss cheese understanding of React at this point.
The problem:
I have an array of metafields containing metafield objects with the following structure:
{
metafields: [
{ 0:
{ namespace: "namespaceVal",
key: "keyVal",
val: [
0: "val1",
1: "val2",
2: "val3"
]
}
},
...
]
}
My code maps metafields into Cards and within each card lives a component <MetafieldInput metafields={metafields['value']} /> and within that component the value array gets mapped to input fields. Overall it looks like:
// App
render() {
const metafields = this.state.metafields;
return (
{metafields.map(metafield) => (
<MetafieldInputs metafields={metafield['value']} />
)}
)
}
//MetafieldInputs
this.state = { metafields: this.props.metafields}
render() {
const metafields = this.state;
return (
{metafields.map((meta, i) => (
<TextField
value={meta}
changeKey={meta}
onChange={(val) => {
this.setState(prevState => {
return { metafields: prevState.metafields.map((field, j) => {
if(j === i) { field = val; }
return field;
})};
});
}}
/>
))}
)
}
Up to this point everything displays correctly and I can change the inputs! However the change happens one at a time, as in I hit a key then I have to click back into the input to add another character. It seems like everything gets re-rendered which is why I have to click back into the input to make another change.
Am I able to use components in this way? It feels like I'm working my way into nesting components but everything I've read says not to nest components. Am I overcomplicating this issue? The only solution I have is to rip out the React portion and take it to pure javascript.
guidance would be much appreciated!
My suggestion is that to out source the onChange handler, and the code can be understood a little bit more easier.
Mainly React does not update state right after setState() is called, it does a batch job. Therefore it can happen that several setState calls are accessing one reference point. If you directly mutate the state, it can cause chaos as other state can use the updated state while doing the batch job.
Also, if you out source onChange handler in the App level, you can change MetafieldInputs into a functional component rather than a class-bases component. Functional based component costs less than class based component and can boost the performance.
Below are updated code, tested. I assume you use Material UI's TextField, but onChangeHandler should also work in your own component.
// Full App.js
import React, { Component } from 'react';
import MetafieldInputs from './MetafieldInputs';
class App extends Component {
state = {
metafields: [
{
metafield:
{
namespace: "namespaceVal",
key: "keyVal",
val: [
{ '0': "val1" },
{ '1': "val2" },
{ '2': "val3" }
]
}
},
]
}
// will never be triggered as from React point of view, the state never changes
componentDidUpdate() {
console.log('componentDidUpdate')
}
render() {
const metafields = this.state.metafields;
const metafieldsKeys = Object.keys(metafields);
const renderInputs = metafieldsKeys.map(key => {
const metafield = metafields[key];
return <MetafieldInputs metafields={metafield.metafield.val} key={metafield.metafield.key} />;
})
return (
<div>
{renderInputs}
</div>
)
}
}
export default App;
// full MetafieldInputs
import React, { Component } from 'react'
import TextField from '#material-ui/core/TextField';
class MetafieldInputs extends Component {
state = {
metafields: this.props.metafields
}
onChangeHandler = (e, index) => {
const value = e.target.value;
this.setState(prevState => {
const updateMetafields = [...prevState.metafields];
const updatedFields = { ...updateMetafields[index] }
updatedFields[index] = value
updateMetafields[index] = updatedFields;
return { metafields: updateMetafields }
})
}
render() {
const { metafields } = this.state;
// will always remain the same
console.log('this.props', this.props)
return (
<div>
{metafields.map((meta, i) => {
return (
<TextField
value={meta[i]}
changekey={meta}
onChange={(e) => this.onChangeHandler(e, i)}
// generally it is not a good idea to use index as a key.
key={i}
/>
)
}
)}
</div>
)
}
}
export default MetafieldInputs
Again, IF you out source the onChangeHandler to App class, MetafieldInputs can be a pure functional component, and all the state management can be done in the App class.
On the other hand, if you want to keep a pure and clean App class, you can also store metafields into MetafieldInputs class in case you might need some other logic in your application.
For instance, your application renders more components than the example does, and MetafieldInputs should not be rendered until something happened. If you fetch data from server end, it is better to fetch the data when it is needed rather than fetching all the data in the App component.
You need to do the onChange at the app level. You should just pass the onChange function into MetaFieldsInput and always use this.props.metafields when rendering

Why isn't `useContext` re-rendering my component?

As per the docs:
When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.
...
A component calling useContext will always re-render when the context value changes.
In my Gatsby JS project I define my Context as such:
Context.js
import React from "react"
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {},
}
const Context = React.createContext(defaultContextValue)
class ContextProviderComponent extends React.Component {
constructor() {
super()
this.setData = this.setData.bind(this)
this.state = {
...defaultContextValue,
set: this.setData,
}
}
setData(newData) {
this.setState(state => ({
data: {
...state.data,
...newData,
},
}))
}
render() {
return <Context.Provider value={this.state}>{this.props.children}</Context.Provider>
}
}
export { Context as default, ContextProviderComponent }
In a layout.js file that wraps around several components I place the context provider:
Layout.js:
import React from 'react'
import { ContextProviderComponent } from '../../context'
const Layout = ({children}) => {
return(
<React.Fragment>
<ContextProviderComponent>
{children}
</ContextProviderComponent>
</React.Fragment>
)
}
And in the component that I wish to consume the context in:
import React, { useContext } from 'react'
import Context from '../../../context'
const Visuals = () => {
const filterByYear = 'year'
const filterByTheme = 'theme'
const value = useContext(Context)
const { filterBy, isOptionClicked, filterValue } = value.data
const data = <<returns some data from backend>>
const works = filterBy === filterByYear ?
data.nodes.filter(node => node.year === filterValue)
:
data.nodes.filter(node => node.category === filterValue)
return (
<Layout noFooter="true">
<Context.Consumer>
{({ data, set }) => (
<div onClick={() => set( { filterBy: 'theme' })}>
{ data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1> }
</div>
)
</Context.Consumer>
</Layout>
)
Context.Consumer works properly in that it successfully updates and reflects changes to the context. However as seen in the code, I would like to have access to updated context values in other parts of the component i.e outside the return function where Context.Consumer is used exclusively. I assumed using the useContext hook would help with this as my component would be re-rendered with new values from context every time the div is clicked - however this is not the case. Any help figuring out why this is would be appreciated.
TL;DR: <Context.Consumer> updates and reflects changes to the context from child component, useContext does not although the component needs it to.
UPDATE:
I have now figured out that useContext will read from the default context value passed to createContext and will essentially operate independently of Context.Provider. That is what is happening here, Context.Provider includes a method that modifies state whereas the default context value does not. My challenge now is figuring out a way to include a function in the default context value that can modify other properties of that value. As it stands:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
set: () => {}
}
set is an empty function which is defined in the ContextProviderComponent (see above). How can I (if possible) define it directly in the context value so that:
const defaultContextValue = {
data: {
filterBy: 'year',
isOptionClicked: false,
filterValue: ''
},
test: 'hi',
set: (newData) => {
//directly modify defaultContextValue.data with newData
}
}
There is no need for you to use both <Context.Consumer> and the useContext hook.
By using the useContext hook you are getting access to the value stored in Context.
Regarding your specific example, a better way to consume the Context within your Visuals component would be as follows:
import React, { useContext } from "react";
import Context from "./context";
const Visuals = () => {
const filterByYear = "year";
const filterByTheme = "theme";
const { data, set } = useContext(Context);
const { filterBy, isOptionClicked, filterValue } = data;
const works =
filterBy === filterByYear
? "filter nodes by year"
: "filter nodes by theme";
return (
<div noFooter="true">
<div>
{data.filterBy === filterByYear ? <h1>Year</h1> : <h1>Theme</h1>}
the value for the 'works' variable is: {works}
<button onClick={() => set({ filterBy: "theme" })}>
Filter by theme
</button>
<button onClick={() => set({ filterBy: "year" })}>
Filter by year
</button>
</div>
</div>
);
};
export default Visuals;
Also, it seems that you are not using the works variable in your component which could be another reason for you not getting the desired results.
You can view a working example with the above implementation of useContext that is somewhat similar to your example in this sandbox
hope this helps.
Problem was embarrassingly simple - <Visuals> was higher up in the component tree than <Layout was for some reason I'm still trying to work out. Marking Itai's answer as correct because it came closest to figuring things out giving the circumstances
In addition to the solution cited by Itai, I believe my problem can help other people here
In my case I found something that had already happened to me, but that now presented itself with this other symptom, of not re-rendering the views that depend on a state stored in a context.
This is because there is a difference in dates between the host and the device. Explained here: https://github.com/facebook/react-native/issues/27008#issuecomment-592048282
And that has to do with the other symptom that I found earlier: https://stackoverflow.com/a/63800388/10947848
To solve this problem, just follow the steps in the first link, or if you find it necessary to just disable the debug mode

jest, enzyme - testing a method that returns jsx

I have the following component:
import React, { Component } from 'react';
import {Link, IndexLink} from 'react-router';
class Navbar extends Component {
renderLinks = (linksData) => {
return linksData.map((linkData) => {
if(linkData.to === '/') {
return(
<div className="navbar-link-container" key={linkData.to}>
<IndexLink activeClassName="navbar-active-link" to={linkData.to}>
<i className="navbar-icon material-icons">{linkData.icon}</i>
<span className="navbar-link-text">{linkData.text}</span>
</IndexLink>
</div>
)
}
else {
return(
<div className="navbar-link-container" key={linkData.to}>
<Link activeClassName="navbar-active-link" to={linkData.to}>
<i className="navbar-icon material-icons">{linkData.icon}</i>
<span className="navbar-link-text">{linkData.text}</span>
</Link>
</div>
)
}
})
};
render() {
return (
<div className={`navbar navbar-${this.props.linksData.length}`}>
{this.renderLinks(this.props.linksData)}
</div>
)
}
}
Navbar.propTypes = {
linksData: React.PropTypes.array.isRequired,
};
export default Navbar;
Now I am trying to write a unit test that will check the if condition (returning IndexLink or Link depending on the .to property):
But I can't seem to test for the exact jsx return of the function, since when I console.log one of the returns I get this:
{ '$$typeof': Symbol(react.element),
type: 'div',
key: '/',
ref: null,
props:
{ className: 'navbar-link-container',
children:
{ '$$typeof': Symbol(react.element),
type: [Object],
key: null,
ref: null,
props: [Object],
_owner: null,
_store: {} } },
_owner: null,
_store: {} }
This is the test I have written so far:
it('renderLinks should return a IndexLink', () => {
const wrapper = shallow(<Navbar linksData={mockLinksData}/>);
const renderLinksReturn = wrapper.instance().renderLinks(mockLinksData);
let foundIndexLink = false;
renderLinksReturn.map((linkHtml) => {
console.log(linkHtml);
});
expect(foundIndexLink).toBe(true);
})
Now I do not know what to test against to see if the function is running correctly. Is there a way to 'mount' the return of the function like a component? Or is there a simple method to return a html string of the actual return that I can check against?
Faced similar issue where we were passing a jsx component to another component as a prop.
You can shallow render the returned jsx since it's like a valid React Function/Stateless Component.
eg:
const renderLinks = shallow(wrapper.instance().renderLinks(mockLinksData))
And continue with your usual enzyme assertions.
To build on top of #Nachiketha 's answer, that syntax won't work when what's returned is a fragment, this can be solved by wrapping the result in a div like:
const renderLinks = shallow(<div>
{wrapper.instance().renderLinks(mockLinksData)
</div>
)}
as suggested in this tread.
I think you don't need to call
const renderLinksReturn = wrapper.instance().renderLinks(mockLinksData);
as it will be called when Navbar will be rendered.
Your solution is correct but in case you want some alternative robust ways to test it.
Since this test specifically tests for IndexLink and assumes that mockLinksData contains to = "/"
it('renderLinks should return a IndexLink when passed a link with to:\"/\"', () => {
const wrapper = shallow(<Navbar linksData={mockLinksData}/>);
// You can use both component name or component displayname
// IndexLink or "IndexLink"
expect(wrapper.find('IndexLink')).to.have.length(1);
// In case you want to test whether indexLink has appropriate props or classes.
const indexLink = wrapper.find(IndexLink).first();
// Check whether indexLink has pass prop
expect(indexLink.props().to).to.equal("/");
// Check whether indexLink has correct classes set.
expect(indexLink.hasClass('navbar-active-link')).to.equal(true);
// Check whether indexLink displays the link test correctly
expect(indexLink.find('navbar-link-text').text()).to.equal(mockLinksData.text);
});
Turns out that the type of the element is stored in the object. So the condition is:
props.children.type.displayName
And the final test I wrote looks like this for IndexLink:
it('renderLinks should return a IndexLink when passed a link with to:\"/\"', () => {
const wrapper = shallow(<Navbar linksData={mockLinksData}/>);
const renderLinksReturn = wrapper.instance().renderLinks(mockLinksData);
let foundIndexLink = false;
renderLinksReturn.map((linkHtml) => {
{linkHtml.props.children.type.displayName === 'IndexLink' ? foundIndexLink = true : null};
});
expect(foundIndexLink).toBe(true);
});

Categories

Resources