How to extract JSX from class render method? - javascript

I want to write something like SCSS for React Native: it'll parse your component jsx and the special SCSS-like styles and return a usual RN component with reworked styles and jsx.
Lets say we have this react code:
class MyClass extends Component {
render() {
return (
<View style={styles.container}>
<Text>I remember syrup sandwiches</Text>
</View>
);
}
}
Also I have SCSS-ish styles where every Text component inside the parent with a container "class" will have the same props that we provided.
const styles = StyleSheet.create(
toRNStyles({
container: {
Text: { color: 'red' },
},
})
);
In the end we need the output of something like this:
...
<View style={styles.container}>
<Text style={styles._Text_container}>
I remember syrup sandwiches
</Text>
</View>
...
So how can I get the jsx that's returning from the render method from outside the class?

You might write a plugin for babel, as react-native uses it to transform JSX to plain javascript.
Have a look to the these packages:
babel-helper-builder-react-jsx
babel-plugin-syntax-jsx
babel-plugin-transform-react-jsx
babel-plugin-transform-react-jsx-source
jsx-ast-utils

There doesn't seem to be a standard way of doing this. However, you could import ReactDOMServer and use its renderToStaticMarkup function.
Like this:
class MyApp extends React.Component {
render() {
var myTestComponent = <Test>bar</Test>;
console.dir(ReactDOMServer.renderToStaticMarkup(myTestComponent));
return myTestComponent;
}
}
const Test = (props) => {
return (
<div>
<p>foo</p>
<span>{props.children}</span>
</div>
);
}
ReactDOM.render(<MyApp />, document.getElementById("myApp"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom-server.js"></script>
<div id="myApp"></div>

I think parsing the returned element is the wrong approach. One challenge will be that the value of style will be an object (styles.container === a hash of style key/values) whereas you need a key which can be mapped to the object.
I think the most reusable approach is to leverage React context (which I'm assuming RN supports!) to build a styleName which can be augmented as you got down the component tree.
Here's an initial approach which makes a few assumptions (e.g. that every component will have styleName provided as a prop; you might want to provide that at design-time rather than run-time). In short, you wrap every component you want to participate with this HOC and the provide styleName as a prop to each component. Those styleName values are concatenated to produce contextualized names which are mapped to styles.
This example produces:
<div style="background-color: green; color: red;">
<div style="color: blue;">Some Text</div>
</div>
const CascadingStyle = (styles, Wrapped) => class extends React.Component {
static displayName = 'CascadingStyle';
static contextTypes = {
styleName: React.PropTypes.string
}
static childContextTypes = {
styleName: React.PropTypes.string
}
// pass the current styleName down the component tree
// to other instances of CascadingStyle
getChildContext () {
return {
styleName: this.getStyleName()
};
}
// generate the current style name by either using the
// value from context, joining the context value with
// the current value, or using the current value (in
// that order).
getStyleName () {
const {styleName: contextStyleName} = this.context;
const {styleName: propsStyleName} = this.props;
let styleName = contextStyleName;
if (propsStyleName && contextStyleName) {
styleName = `${contextStyleName}_${propsStyleName}`;
} else if (propsStyleName) {
styleName = propsStyleName;
}
return styleName;
}
// if the component has styleName, find that style object and merge it with other run-time styles
getStyle () {
if (this.props.styleName) {
return Object.assign({}, styles[this.getStyleName()], this.props.styles);
}
return this.props.styles;
}
render () {
return (
<Wrapped {...this.props} style={this.getStyle()} />
);
}
};
const myStyles = {
container: {backgroundColor: 'green', color: 'red'},
container_text: {color: 'blue'}
};
const Container = CascadingStyle(myStyles, (props) => {
return (
<div {...props} />
);
});
const Text = CascadingStyle(myStyles, (props) => {
return (
<div {...props} />
);
});
const Component = () => {
return (
<Container styleName="container">
<Text styleName="text">Some Text</Text>
</Container>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

Related

Map of refs, current is always null

I'm creating a host of a bunch of pages, and those pages are created dynamically. Each page has a function that I'd like to call at a specific time, but when trying to access a ref for the page, the current is always null.
export default class QuizViewPager extends React.Component<QuizViewPagerProps, QuizViewPagerState> {
quizDeck: Deck | undefined;
quizRefMap: Map<number, React.RefObject<Quiz>>;
quizzes: JSX.Element[] = [];
viewPager: React.RefObject<ViewPager>;
constructor(props: QuizViewPagerProps) {
super(props);
this.quizRefMap = new Map<number, React.RefObject<Quiz>>();
this.viewPager = React.createRef<ViewPager>();
this.state = {
currentPage: 0,
}
for (let i = 0; i < this.quizDeck!.litems.length; i++) {
this.addQuiz(i);
}
}
setQuizPage = (page: number) => {
this.viewPager.current?.setPage(page);
this.setState({ currentPage: page })
this.quizRefMap.get(page)?.current?.focusInput();
}
addQuiz(page: number) {
const entry = this.quizDeck!.litems[page];
var ref: React.RefObject<Quiz> = React.createRef<Quiz>();
this.quizRefMap.set(page, ref);
this.quizzes.push(
<Quiz
key={page}
litem={entry}
index={page}
ref={ref}
pagerFocusIndex={this.state.currentPage}
pagerLength={this.quizDeck?.litems.length!}
setQuizPage={this.setQuizPage}
navigation={this.props.navigation}
quizType={this.props.route.params.quizType}
quizManager={this.props.route.params.quizType === EQuizType.Lesson ? GlobalVars.lessonQuizManager : GlobalVars.reviewQuizManager}
/>
)
}
render() {
return (
<View style={{ flex: 1 }}>
<ViewPager
style={styles.viewPager}
initialPage={0}
ref={this.viewPager}
scrollEnabled={false}
>
{this.quizzes}
</ViewPager>
</View >
);
}
};
You can see in addQuiz() I am creating a ref, pushing it into my map, and passing that ref into the Quiz component. However, when attempting to access any of the refs in setQuizPage(), the Map is full of refs with null current properties.
To sum it up, the ViewPager library being used isn't actually rendering the children you are passing it.
If we look at the source of ViewPager (react-native-viewpager), we will see children={childrenWithOverriddenStyle(this.props.children)} (line 182). If we dig into the childrenWithOverriddenStyle method, we will see that it is actually "cloning" the children being passed in via React.createElement.
It is relatively easy to test whether or not the ref passed to these components will be preserved by creating a little demo:
const logRef = (element) => {
console.log("logRef", element);
};
const ChildrenCreator = (props) => {
return (
<div>
{props.children}
{React.Children.map(props.children, (child) => {
console.log("creating new", child);
let newProps = {
...child.props,
created: "true"
};
return React.createElement(child.type, newProps);
})}
</div>
);
};
const App = () => {
return (
<div className="App">
<ChildrenCreator>
<h1 ref={logRef}>Hello World</h1>
<p>It's a nice day!</p>
</ChildrenCreator>
</div>
);
}
ReactDOM.render(<App />, '#app');
(codesandbox)
If we look at the console when running this, we will be able to see that the output from logRef only appears for the first, uncopied h1 tag, and not the 2nd one that was copied.
While this doesn't fix the problem, this at least answers the question of why the refs are null in your Map. It actually may be worth creating an issue for the library in order to swap it to React.cloneElement, since cloneElement will preserve the ref.

React HOC working on some but not other components

I'm using a HOC component to bind an action to many different types of element, including SVG cells, which, when an onClick is bound normally, it works, but when I use my HOC it returns un-intended results.
Minimally reproducible example: https://codesandbox.io/s/ecstatic-keldysh-3viw0
The HOC component:
export const withReport = Component => ({ children, ...props }) => {
console.log(Component); //this only prints for ListItem elements for some reason
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
console.log('clicked!'); //even this wont work on some.
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}>
{children}
</Component>
);
};
Usage working:
const ListItemWIthReport = withReport(ListItem); //list item from react-mui
{items.map((item, key) => (
<ListItemWithReport report={item.report} key={key} button>
{/* listitem children*/}
</ListItemWithReport>
))}
Usage not working:
const BarWithReport = withReport(Bar); //Bar from recharts
{bars.map((bar, index) => (
<BarWithReport
report={bar.report}
key={index}
dataKey={bar.name}
fill={bar.fill}
/>
))}
The ListItem works 100% as anticipated, however, the bars will not render inside of the BarChart. Similarly, with a PieChart the Cells will actually render, with the correct sizes according to their values, however, props like "fill" do not appear to pass down.
Am I using the HOC incorrectly? I don't see an option other than HOC for the inside of Charts as many types of elements will be considered invalid HTML?
You might be dealing with components that have important static properties that need to be hoisted into the wrapped component or need to have ref forwarding implemented in order for their parent components to handle them. Getting these pieces in place is important, especially when wrapping components where you don't know their internals. That Bar component, for example, does have some static properties. Your HOC is making those disappear.
Here's how you can hoist these static members:
import hoistNonReactStatic from 'hoist-non-react-statics';
export const withReport = Component => {
const EnhancedComponent = props => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return (
<Component onClick={handleClick} {...props}/>
);
};
hoistNonReactStatic(EnhancedComponent, Component);
return EnhancedComponent;
};
Docs on hoisting statics and ref forwarding can be found in this handy guide to HOCs.
There may be some libraries that can take care of all these details for you. One, addhoc, works like this:
import addHOC from 'addhoc';
export const withReport = addHOC(render => {
const { dispatch } = useContext(DashboardContext);
const handleClick = () => {
const { report } = props;
if (typeof report === "undefined") return false;
dispatch({ type: SET_ACTIVE_REPORT, activeReport: report });
dispatch({ type: TOGGLE_REPORT });
};
return render({ onClick: handleClick });
});
Of course, if the parent component is checking child components by type explicitly, then you won't be able to use HOCs at all. In fact, it looks like recharts has that issue. Here you can see the chart is defined in terms of child components which are then searched for explicitly by type.
I think your HOC is invalid, because not every wrapper-Component (e.g. HTML element) is basically clickable. Maybe this snipped can clarify what I am trying to say:
const withReport = Component => (props) => {
const handleClick = () => console.log('whatever')
// Careful - your component might not support onClick by default
return <Component onClick={handleClick} {...props} />
// vs.
return <div onClick={handleClick} style={{backgroundColor: 'green'}}>
<Component {...props} />
{props.children}
</div>
}
// Your import from wherever you want
class SomeClass extends React.Component {
render() {
return <span onClick={this.props.onClick}>{this.props.children}</span>
// vs.
return <span style={{backgroundColor: 'red'}}>
{
// Careful - your imported component might not support children by default
this.props.children
}
</span>
}
}
const ReportedListItem = withReport(SomeClass)
ReactDOM.render(<ReportedListItem>
<h2>child</h2>
</ReportedListItem>, mountNode)
You can have the uppers or the unders (separated by vs.) but not crossed. The HOC using the second return (controlled wrapper-Component) is sure more save.
I've used 4 methods successfully to wrap Recharts components.
First Method
Wrap the component in a HOC and use Object.Assign with some overloads. This breaks some animation and difficult to use an active Dot on lines. Recharts grabs some props from components before rendering them. So if the prop isn't passed into the HOC, then it won't render properly.
...
function LineWrapper({
dataOverload,
data,
children,
strokeWidth,
strokeWidthOverload,
isAnimationActive,
dot,
dotOverload,
activeDot,
activeDotOverload,
...rest
}: PropsWithChildren<Props>) {
const defaultDotStroke = 12;
return (
<Line
aria-label="chart-line"
isAnimationActive={false}
strokeWidth={strokeWidthOverload ?? 2}
data={dataOverload?.chartData ?? data}
dot={dotOverload ?? { strokeWidth: defaultDotStroke }}
activeDot={activeDotOverload ?? { strokeWidth: defaultDotStroke + 2 }}
{...rest}
>
{children}
</Line>
);
}
export default renderChartWrapper(Line, LineWrapper, {
activeDot: <Dot r={14} />,
});
const renderChartWrapper = <P extends BP, BP = {}>(
component: React.ComponentType<BP>,
wrapperFC: React.FC<P>,
defaultProps?: Partial<P>
): React.FC<P> => {
Object.assign(wrapperFC, component);
if (defaultProps) {
wrapperFC.defaultProps = wrapperFC.defaultProps ?? {};
Object.assign(wrapperFC.defaultProps, defaultProps);
}
return wrapperFC;
};
Second Method
Use default props to assign values. Any props passed into the HOC will be overridden.
import { XAxisProps } from 'recharts';
import { createStyles } from '#material-ui/core';
import { themeExtensions } from '../../../assets/theme';
const useStyles = createStyles({
tickStyle: {
...themeExtensions.font.graphAxis,
},
});
type Props = XAxisProps;
// There is no actual implementation of XAxis. Recharts render function grabs the props only.
function XAxisWrapper(props: Props) {
return null;
}
XAxisWrapper.displayName = 'XAxis';
XAxisWrapper.defaultProps = {
allowDecimals: true,
hide: false,
orientation: 'bottom',
width: 0,
height: 30,
mirror: false,
xAxisId: 0,
type: 'category',
domain: [0, 'auto'],
padding: { left: 0, right: 0 },
allowDataOverflow: false,
scale: 'auto',
reversed: false,
allowDuplicatedCategory: false,
tick: { style: useStyles.tickStyle },
tickCount: 5,
tickLine: false,
dataKey: 'key',
};
export default XAxisWrapper;
Third Method
I didn't like this so I've worked around it, but you can extend the class.
export default class LineWrapper extends Line {
render(){
return (<Line {...this.props} />
}
}
Fourth Method
I don't have a quick example of this, but I always render the shape or children and provide functions to help. For example, for bar cells I use this:
export default function renderBarCellPattern(cellOptions: CellRenderOptions) {
const { data, fill, match, pattern } = cellOptions;
const id = _uniqueId();
const cells = data.map((d) =>
match(d) ? (
<Cell
key={`cell-${id}`}
strokeWidth={4}
stroke={fill}
fill={`url(#bar-mask-pattern-${id})`}
/>
) : (
<Cell key={`cell-${id}`} strokeWidth={2} fill={fill} />
)
);
return !pattern
? cells
: cells.concat(
<CloneElement<MaskProps>
key={`pattern-${id}`}
element={pattern}
id={`bar-mask-pattern-${id}`}
fill={fill}
/>
);
}
// and
<Bar {...requiredProps}>
{renderBarCellPattern(...cell details)}
</Bar>
CloneElement is just a personal wrapper for Reacts cloneElement().

React Native Conditional Rendering without if statement

Is it possible to re-render specific components without using if/else statement in the rendersection.
So when a specific statement changed only his specific component(s) will re-render
while the rest remain intact.
Because if I use the componentWillUpdate or shouldComponentUpdate it will re-render the whole app scene again.
I look forward to your answers.
You can try something like -
class MainComponent extends React.Component {
displayMessage() {
if (this.props.isGreeting) {
return <Text> Hello, JSX! </Text>;
} else {
return <Text> Goodbye, JSX! </Text>;
}
}
render() {
return ( <View> { this.displayMessage() } </View> );
}
}
Check this article - https://medium.com/#szholdiyarov/conditional-rendering-in-react-native-286351816db4
You can try with new ES6 enhanced object litrals.
We can access the property of an object using bracket [] notation:
myObj = { "name":"Stan", "age":26, "car": "Lamborghini"};
x = myObj["name"]; //x will contain Stan
So you can use this approach for conditional rendering
this.state = {
contentToDisplay: "content1",
}
render() {
return (
<section>
{{
content1: <Content1 />,
content2: <Content2 />,
content3: <Content3 />,
}[this.state.contentToDisplay]}
</section>
);
}

Using enzyme, How to find a child component in a component react-native

I am new to React-native and enzyme, I am trying to create a custom component here.
I will be displaying an Image based on this.props.hasIcon. I set default props value for hasIcon as true. When I check Image exists in enzyme ShallowWrapper. I am getting false.
tlProgress.js
class TLProgress extends Component {
render() {
return (
<View style={styles.container}>
{this.renderImage}
{this.renderProgress}
</View>
);
}
}
TLProgress.defaultProps = {
icon: require("./../../img/logo.png"),
indeterminate: true,
progressColor: Colors.TLColorAccent,
hasIcon: true,
progressType: "bar"
};
and renderImage() has the Image
renderImage() {
if (this.props.hasIcon) {
return <Image style={styles.logoStyle} source={this.props.icon} />;
}
}
Now, If I check Image exists in enzyme am getting false.
tlProgress.test.js
describe("tlProgress rendering ", () => {
let wrapper;
beforeAll(() => {
props = { indeterminate: false };
wrapper = shallow(<TLProgress {...props} />);
});
it("check progress has app icon", () => {
expect(wrapper.find("Image").exists()).toBe(true); // fails here..
});
});
You are not calling the renderImage function in your render() -- you forgot the brackets, thus it is being interpreted as an undefined variable.
It should be: (I am assuming you want to call renderProgress() and not renderProgress as well)
class TLProgress extends Component {
render() {
return (
<View style={styles.container}>
{this.renderImage()}
{this.renderProgress()}
</View>
);
}
You're searching for a tag called 'Image' instead of looking for your component Image
It should be :
import Image from '../Image';
wrapper.find(Image) //do your assert here

How to Access styles from React?

I am trying to access the width and height styles of a div in React but I have been running into one problem. This is what I got so far:
componentDidMount() {
console.log(this.refs.container.style);
}
render() {
return (
<div ref={"container"} className={"container"}></div> //set reff
);
}
This works but the output that I get is a CSSStyleDeclaration object and in the all property I can all the CSS selectors for that object but they none of them are set. They are all set to an empty string.
This is the output of the CSSStyleDecleration is: http://pastebin.com/wXRPxz5p
Any help on getting to see the actual styles (event inherrited ones) would be greatly appreciated!
Thanks!
For React v <= 15
console.log( ReactDOM.findDOMNode(this.refs.container).style); //React v > 0.14
console.log( React.findDOMNode(this.refs.container).style);//React v <= 0.13.3
EDIT:
For getting the specific style value
console.log(window.getComputedStyle(ReactDOM.findDOMNode(this.refs.container)).getPropertyValue("border-radius"));// border-radius can be replaced with any other style attributes;
For React v>= 16
assign ref using callback style or by using createRef().
assignRef = element => {
this.container = element;
}
getStyle = () => {
const styles = this.container.style;
console.log(styles);
// for getting computed styles
const computed = window.getComputedStyle(this.container).getPropertyValue("border-radius"));// border-radius can be replaced with any other style attributes;
console.log(computed);
}
Here is an example of computing the CSS property value via React Refs and .getComputedStyle method:
class App extends React.Component {
constructor(props) {
super(props)
this.divRef = React.createRef()
}
componentDidMount() {
const styles = getComputedStyle(this.divRef.current)
console.log(styles.color) // rgb(0, 0, 0)
console.log(styles.width) // 976px
}
render() {
return <div ref={this.divRef}>Some Text</div>
}
}
It's worth noting that while ReactDOM.findDOMNode is usable today, it will be deprecated in the future in place of callback refs.
There is a post here by Dan Abramov which outlines reasons for not using findDOMNode while providing examples of how to replace the use of ReactDOM.findDOMNode with callback refs.
Since I've seen SO users get upset when only a link was included in an answer, so I will pass along one of the examples Dan has kindly provided:
findDOMNode(stringDOMRef)
**Before:**
class MyComponent extends Component {
componentDidMount() {
findDOMNode(this.refs.something).scrollIntoView();
}
render() {
return (
<div>
<div ref='something' />
</div>
)
}
}
**After:**
class MyComponent extends Component {
componentDidMount() {
this.something.scrollIntoView();
}
render() {
return (
<div>
<div ref={node => this.something = node} />
</div>
)
}
}
You should use ReactDOM.findDOMNode method and work from there. Here's the code that does what you need.
var Hello = React.createClass({
componentDidMount: function() {
var elem = ReactDOM.findDOMNode(this.refs.container);
console.log(elem.offsetWidth, elem.offsetHeight);
},
render: function() {
return (
<div ref={"container"} className={"container"}>
Hello world
</div>
);
}
});
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
jsFiddle
In case of function components use the useRef Hook:
const _optionsButton = useRef(null);
const _onSelectText = (event) => {
if (true) {
_optionsButton.current.style["display"] = "block";
} else {
_optionsButton.current.style["display"] = "none";
}
console.log(_optionsButton.current.style); //all styles applied to element
};
add ref attribute to component
<IconButton
style={{
color: "rgba(0, 0, 0, 0.54)",
fill: "rgba(0, 0, 0, 0.54)",
border: "1px solid rgba(0, 0, 0, 0.54)",
position: "absolute",
display: "none",
}}
color="primary"
component="span"
onClick={() => {}}
ref={_optionsButton} //this
>
Check
</IconButton>;
Thank you
You already get the style, the reason why CSSStyleDeclaration object's props have so much empty string value is it's link to the inner style.
See what will happen if you make change like below:
<div ref={"container"} className={"container"} style={{ width: 100 }}></div>

Categories

Resources