let's consider a list of modules imported arbitrarily as so :
/**
* modules is a list of absolute paths to modules exporting react components
*/
const getAllComponents = async(modules) => {
const components = [];
modules.forEach((moduleName) => {
try {
const module = await import(moduleName);
components.push(module.default);
}catch(err) {
console.warn(err.message)
}
})
return components;
}
and a parent react component in project and a random component exported from a disk based module:
// my-component.js
function MyComponent({moduleNames}) {
const [components, setComponents] = useState([]);
useEffect(() => getAllComponents.then(setComponents), []);
// rendering a random component assuming it exists
const RenderedComponent = components[0];
return (
<div>
{/* failling here: */}
<RenderedComponent />
</div>
)
}
// a-random-component.js (disk based module whose path is in moduleNames in above component props)
function RandomComponent() {
return (<div>propless component</div>)
}
I get the following error when compiling:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Check the render method of `MyComponent`.
Is there a way to render arbitrary component in the dom without having to use the static import statement ?
Related
I am using nuxt 3 and Compositions API.
I get such a nesting, how to get rid of the extra svg tag?
I would also like to receive svg attributes whenever possible and change, for example, fill
template
<template>
<div>
<component :is="render"></component>
</div>
</template>
Script
import { h } from "vue";
const { data, pending, error, refresh } = await useFetch(svgURL);
const getDataVal = data.value
const SvgToRaw = await getDataVal.text();
const render = () => {
return h("svg", {
class: "bar",
innerHTML: SvgToRaw,
});
};
Chrome Dev Tools
I tried to create a virtual DOM tree and get an HTML element from there, not text, but I think this is a bad solution
import hv from "virtual-dom/h";
import diff from "virtual-dom/diff";
import patch from "virtual-dom/patch";
import createElement from "virtual-dom/create-element";
const betaRender = hv("span", { innerHTML: svgString });
var rootNode = createElement(betaRender);
var patches = diff(rootNode);
return patches[0].vNode.innerHTML
SSR support is important to me so I can't use standard tools
I'm using tiptap and trying to extend the Paragraph node to wrap some extra stuff around its view. I used <NodeViewWrapper> and <NodeViewContent> as the guides said.
const ParagraphWrapper = () => {
return (
<NodeViewWrapper>
<NodeViewContent />
</NodeViewWrapper>
)
}
const ParagraphExt = Paragraph.extend({
addNodeView() {
return ReactNodeViewRenderer(ParagraphWrapper)
}
})
export default function App() {
const editor = useEditor({
extensions: [
Document,
Text,
ParagraphExt, // <<<< text-align was not rendered
// Paragraph, // <<<< This worked
TextAlign.configure({
types: ["paragraph"]
}),
],
content: `<p style="text-align: center">This is a paragraph</p>`,
})
return (
<>
<EditorContent editor={editor} />
<pre>{JSON.stringify(editor?.getJSON?.(), null, 2)}</pre>
</>
);
}
However, this seems to render the node from scratch. Thus, other extensions, such as textAlign no longer works.
I only need to wrap a thin layer around whatever was rendered originally. How do I do that?
Code Sandbox
You still get access to the attrs being passed to the node which is available in props. You can use that info to style your rendered content as you wish.
const ParagraphWrapper = (props) => {
const textAlign = props.node.attrs.textAlign;
return (
<NodeViewWrapper>
<NodeViewContent style={{textAlign}} />
</NodeViewWrapper>
);
};
It seems that if you don't inject Material-UI stylesheets into a jest/react-testing-library test then jsdom will fail to get the correct styles from your components (e.g. running getComputedStyle(component) will return the incorrect styles for the component).
How you properly setup a jest/react-testing-library test so that the styles are correctly injected into the test? I've already wrapped the components in a theme provider, which works fine.
As a workaround reinserting the whole head (or the element where JSS styles are injected) before assertion seems to apply styles correctly with both getComputedStyle() and react testing library's toHaveStyle():
import React from "react";
import "#testing-library/jest-dom/extend-expect";
import { render } from "#testing-library/react";
test("test my styles", () => {
const { getByTestId } = render(
<div data-testid="wrapper">
<MyButtonStyledWithJSS/>
</div>
);
const button = getByTestId("wrapper").firstChild;
document.head.innerHTML = document.head.innerHTML;
expect(button).toHaveStyle(`border-radius: 4px;`);
});
This will still fail though when you're using dynamic styles, like:
myButton: {
padding: props => props.spacing,
...
}
That's because JSS uses CSSStyleSheet.insertRule method to inject these styles, and it won't appear as a style node in the head. One solution to this issue is to hook into the browser's insertRule method and add incoming rules to the head as style tags. To extract all this into a function:
function mockStyleInjection() {
const defaultInsertRule = window.CSSStyleSheet.prototype.insertRule;
window.CSSStyleSheet.prototype.insertRule = function (rule, index) {
const styleElement = document.createElement("style");
const textNode = document.createTextNode(rule);
styleElement.appendChild(textNode);
document.head.appendChild(styleElement);
return defaultInsertRule.bind(this)(rule, index);
};
// cleanup function, which reinserts the head and cleans up method overwrite
return function applyJSSRules() {
window.CSSStyleSheet.prototype.insertRule = defaultInsertRule;
document.head.innerHTML = document.head.innerHTML;
};
}
Example usage of this function in our previous test:
import React from "react";
import "#testing-library/jest-dom/extend-expect";
import { render } from "#testing-library/react";
test("test my styles", () => {
const applyJSSRules = mockStyleInjection();
const { getByTestId } = render(
<div data-testid="wrapper">
<MyButtonStyledWithJSS spacing="8px"/>
</div>
);
const button = getByTestId("wrapper").firstChild;
applyJSSRules();
expect(button).toHaveStyle("border-radius: 4px;");
expect(button).toHaveStyle("padding: 8px;");
});
This ultimately seems like an issue with JSS and various browser implementations like jsdom and and Blink (at least in Chrome). You can see it in Chrome when trying to modify/enable/disable these style rules (you can't).
The behavior appears to be a result of the JSS library using the CSSOM insertRule API. There's a stylesheet generated in the DOM for the styles we expect in our component, but the tag is empty - it's just used to link the shadow CSS back to the DOM. The styles are never written to the inline stylesheet in the DOM, and as a result, the getComputedStyle method does not return the expected results.
There's an open issue to address this behavior and make development easier.
I switched my custom components to styled-components, which does not have some of these idiosyncrasies.
Material-UI is planning on transitioning soon as well.
You could add this to a custom render function. After rendering, the function pulls the styles out of cssom and puts them into a style tag. Here is an implementation:
let customRender = (ui, options) => {
let renderResult = render(ui, options);
let styleElement = document.createElement("style");
let styleText = "";
for (let styleSheet of document.styleSheets) {
for (let rule of styleSheet.cssRules) {
styleText += rule.cssText + "\n";
}
}
styleElement.textContent = styleText.slice(0, -1);
document.head.appendChild(styleElement);
// remove old style elements
let emptyStyleElements = document.head.querySelectorAll('style[data-jss=""]');
for (let element of emptyStyleElements) {
element.remove();
}
return renderResult;
}
I can't speak specifically to Material-UI stylesheets, but you can inject a stylesheet into rendered component:
import {render} from '#testing-library/react';
import fs from 'fs';
import path from 'path';
const stylesheetFile = fs.reactFileSync(path.resolve(__dirname, '../path-to-stylesheet'), 'utf-8');
const styleTag = document.createElement('style');
styleTag.type = 'text/css';
styleTag.innerHTML = stylesheetFile;
const rendered = render(<MyComponent>);
rendered.append(style);
You don't necessarily have to read from a file, you can use whatever text you want.
I need access the instance of children component:
const wrapper = shallow(
<Provider store={store}>
<CustomForm />
</Provider>
);
I need to access CustomForm instance .. how can I do this?
I tried:
const instance = wrapper
.children(0)
.instance();
If your test needs to access the instances of child components then you will need to use mount() instead of shallow().
shallow() does not do a full DOM rendering and creates an instance for the root element if it is declared using a class (note that functional components never have instances), but it does not create instances for child components.
mount() does a full DOM rendering and creates instances for all components (root or child) declared using classes.
In your example you are using chilren(0) to access the ReactWrapper, alternatives are childAt(0), or something like find('Component').at(0).
Here is a simple working example:
import * as React from 'react';
import { mount } from 'enzyme';
class Component extends React.Component {
componentMethod = () => 'componentMethod called'
render() {
return (
<span>The Component</span>
);
}
}
test('get component instance', () => {
const component = mount(
<div>
<Component />
</div>
);
const componentInstance = component.find('Component').at(0).instance();
expect(componentInstance.componentMethod()).toBe('componentMethod called'); // PASSES
});
I wrote a function to build an HTML form based on the keys and values of an object and I am trying to return the form in my render method. However, I keep getting the error:
ReactJs 0.14 - Invariant Violation: Objects are not valid as a React child
Here is my createForm() method:
createForm() {
const obj = {
}
const object_fields = resourceFields.fields;
let form = document.createElement('form');
_.forIn(object_fields, function(field_value, field_name) {
let div = document.createElement('div');
div.setAttribute('className', 'form-control');
let label = document.createElement('label');
label.setAttribute('htmlFor', 'name');
label.innerHTML = field_name;
let input = document.createElement('input');
input.setAttribute('className', 'form-control');
input.setAttribute('type', 'text');
input.setAttribute('ref', field_name);
input.setAttribute('id', field_name);
input.setAttribute('value', field_value);
input.setAttribute('onChange', '{this.handleChange}');
div.appendChild(label);
div.appendChild(input);
form.appendChild(div);
})
console.log(form) //this prints out fine
return form
}
Here is my render() method:
render() {
return (
<div>
{this.createForm()}
</div>
)
}
Does anyone know what might be happening? My form prints out in the console just fine... Thanks in advance!
You never manipulate actual DOM nodes when you're working with React. When you build your UI in the render function, the JSX markup is translated into plain JavaScript (React.createElement function calls), which builds a representation of the DOM.
So, in your case, you should return JSX in createForm, not a DOM element.