Setting custom props on a dynamically created React component - javascript

I'm refactoring some of my React code for ease of use in places where I can't use Babel directly (such as in short embedded JavaScript on pages). To assist with this I'm setting up a short function that builds the components and passes props to them. This code works just fine:
components.js:
import ResponsiveMenu from './components/responsive-menu';
window.setupMenu = (items, ele) => {
ReactDOM.render(<ResponsiveMenu items={items}/>, ele);
};
static-js.html:
<div id="menu"></div>
<script>
setupMenu({ items: [] }, document.getElementById('menu');
</script>
However, when I attempt to turn it into something more generic to handle more components like so:
components.js:
import ResponsiveMenu from './components/responsive-menu';
import AnotherComp from './components/another-comp';
window.setupComponent = (selector, name, props) => {
let eles;
if (typeof selector == 'string') {
eles = [];
let nl = document.querySelectorAll(selector), node;
for (let i = 0; node = nl[i]; i++) { eles.push(node); }
} else {
eles = $.toArray(selector); // A helper function that converts any value to an array.
}
return eles.map (
(ele) => {
let passProps = typeof props == 'function' ? props(ele) : props;
return ReactDOM.render(React.createElement(name, passProps), ele);
}
);
};
static-js.html:
<div id="menu"></div>
<script>
setupComponent('#menu', 'ResponsiveMenu', { items: [] });
</script>
I then get this error: Warning: Unknown prop "items" on <ResponsiveMenu> tag. Remove this prop from the element. For details, see (really unhelpful shortened link that SO doesn't want me posting)
Please help me understand why this works for the JSX version and not for the more manual version of creating the component.

When you pass string parameter to React.createElement, it will create native DOM element and there is no valid html DOM ResponsiveMenu.
You can store element into hash and store it into window variable.
Example:
// store component into window variable
window.components = {
ResponsiveMenu: ResponsiveMenu
}
//extract component from window variable by name
React.createElement(window.components[name], passProps)

Related

Is there a way to reference a DOM element in Svelte component created using Client-side component API?

I am creating a svelte component in use:action function and I need to get the DOM element from the svelte component. I can't figure out how to bind or just get reference to DOM element in this scenario. One way I thought was to give the element a unique attribute, class or any identifier to use document.querySelect grab it but I feel like there should be way to get the element since I just create it right there. I am looking for api to equivalent of bind:this.
Example code
import ToolTip from '$lib/components/tooltips/Plain.svelte';
export default function (node: HTMLElement) {
const title = node.getAttribute('title');
let tooltipComponent = new ToolTip({
props: {
title,
},
target: node,
});
const tooltip = undefined; // ?? how to get the DOM element here
let arrow = tooltip.querySelector('#tooltip-arrow');
let update = () => {
compute(node, tooltip, arrow);
};
node.addEventListener('mouseenter', update);
node.addEventListener('focus', update);
return {
update() {
},
destroy() {
node.removeEventListener('mouseenter', update);
node.removeEventListener('focus', update);
tooltipComponent.$destroy();
},
};
}
One way I thought to do it
const uniqueId = generateUniqueId();
let tooltipComponent = new ToolTip({
props: {
title,
class: `tooltip-${uniqueId}`,
},
target: node,
});
const tooltip = tooltip.querySelector(`.tooltip-${uniqueId}`);
Alternatively to querying the element a reference inside the component could be set with bind:this and either made accessible via <svelte:options accessors={true}/> or a function REPL
<script>
import Tooltip from './Tooltip.svelte'
function addTooltip(node) {
const title = node.getAttribute('title');
const tooltipComp = new Tooltip({ target: node , props: {title}});
const tooltipElem = tooltipComp.tooltip
tooltipElem.style.background = 'lightblue'
const tooltipElemFn = tooltipComp.getTooltipElem()
tooltipElemFn.style.border = '2px solid purple'
}
</script>
<div use:addTooltip title="tooltip">
I have a tooltip
</div>
<svelte:options accessors={true}/>
<script>
export let title
// export variable with option accessors={true} OR make accessible via function
export let tooltip
export function getTooltipElem() {
return tooltip
}
</script>
<div bind:this={tooltip}>
{title}
</div>
There is no guaranteed way of finding any elements because components do not necessarily create any. Using some unique identifier or the DOM hierarchy can work if you know the structure of the component you create.
Just make sure to look within the target that you mount the component on. So use something like node.querySelector(...).
REPL example

Why isn't the mobx #computed value?

Simple: the computed value isn't updating when the observable it references changes.
import {observable,computed,action} from 'mobx';
export default class anObject {
// THESE WRITTEN CHARACTERISTICS ARE MANDATORY
#observable attributes = {}; // {attribute : [values]}
#observable attributeOrder = {}; // {attribute: order-index}
#observable attributeToggle = {}; // {attribute : bool}
#computed get orderedAttributeKeys() {
const orderedAttributeKeys = [];
Object.entries(this.attributeOrder).forEach(
([attrName, index]) => orderedAttributeKeys[index] = attrName
);
return orderedAttributeKeys;
};
changeAttribute = (existingAttr, newAttr) => {
this.attributes[newAttr] = this.attributes[existingAttr].slice(0);
delete this.attributes[existingAttr];
this.attributeOrder[newAttr] = this.attributeOrder[existingAttr];
delete this.attributeOrder[existingAttr];
this.attributeToggle[newAttr] = this.attributeToggle[existingAttr];
delete this.attributeToggle[existingAttr];
console.log(this.orderedAttributeKeys)
};
}
After calling changeAttribute, this.orderedAttributeKeys does not return a new value. The node appears unchanged.
However, if I remove the #computed and make it a normal (non-getter) function, then for some reason this.orderedAttributeKeys does display the new values. Why is this?
EDIT: ADDED MORE INFORMATION
It updates judging by logs and debugging tools, but doesn't render on the screen (the below component has this code, but does NOT re-render). Why?
{/* ATTRIBUTES */}
<div>
<h5>Attributes</h5>
{
this.props.appStore.repo.canvas.pointerToObjectAboveInCanvas.orderedAttributeKeys.map((attr) => { return <Attribute node={node} attribute={attr} key={attr}/>})
}
</div>
pointerToObjectAboveInCanvas is a variable. It's been set to point to the object above.
The changeAttribute function in anObject is called in this pattern. It starts in the Attribute component with this method
handleAttrKeyChange = async (existingKey, newKey) => {
await this.canvas.updateNodeAttrKey(this.props.node, existingKey, newKey);
this.setState({attributeEdit: false}); // The Attribute component re-renders (we change from an Input holding the attribute prop, to a div. But the above component which calls Attribute doesn't re-render, so the attribute prop is the same
};
which calls this method in another object (this.canvas)
updateNodeAttrKey = (node, existingKey, newKey) => {
if (existingKey === newKey) { return { success: true } }
else if (newKey === "") { return { success: false, errors: [{msg: "If you'd like to delete this attribute, click on the red cross to the right!"}] } }
node.changeAttribute(existingKey, newKey);
return { success: true }
};
Why isn't the component that holds Attribute re-rendering? It's calling orderedAttributeKeys!!! Or am I asking the wrong question, and something else is the issue...
An interesting fact is this same set of calls happens for changing the attributeValue (attribute is the key in anObject's observable dictionary, attributeValue is the value), BUT it shows up (because the Attribute component re-renders and it pulls directly from the node's attribute dictionary to extract the values. Again, this is the issue, an attribute key changes but the component outside it doesn't re-render so the attribute prop doesn't change?!!!
It is because you have decorated changeAttribute with the #action decorator.
This means that all observable mutations within that function occur in a single transaction - e.g. after the console log.
If you remove the #action decorator you should see that those observables get updated on the line they are called and your console log should be as you expect it.
Further reading:
https://mobx.js.org/refguide/action.html
https://mobx.js.org/refguide/transaction.html
Try to simplify your code:
#computed
get orderedAttributeKeys() {
const orderedAttributeKeys = [];
Object.entries(this.attributeOrder).forEach(
([attrName, index]) => orderedAttributeKeys[index] = this.attributes[attrName])
);
return orderedAttributeKeys;
};
#action.bound
changeAttribute(existingAttr, newAttr) {
// ...
};
Also rename your Store name, Object is reserved export default class StoreName

What do you call this React Component pattern?

I am having a hard time finding some documentation on this pattern.
Does it have a name?
TextBase is a styled component. so I can extend it as following:
Text.H1 = withComponent('h1') however I want html attributes to be passed as well. Hence the function component. However when I extend my Text component the props are being overridden, resulting with all components being h1's.
const Text = (props) => {
const { children, testid, ...rest } = props;
return <TextBase data-testid={testid} {...rest}>{children}</TextBase>
}
Text.defaultProps = {color: 'red'}
Text.H1 = Text
Text.H1.defaultProps = { as: 'h1'}
Text.H2 = Text
Text.H2.defaultProps = { as: 'h2'}
Text.H3 = Text
Text.H3.defaultProps = { as: 'h3'}
🙏🏽
Try binding the function call using this or use arrow function or manually bind the function using bind. I am sure it will work. Reference fault is all this is

VueJS $set with variable keypath

I'm currently working on a simple filemanager component which I trigger from parent component. After selecting media in the filemanager I $dispatch a simple data object with 2 keys: element & media. I use element to keep track where I want the media to be appended to my current data object and media has the media information (id, type, name and so on). This setup gives me some trouble when I want to $set the media data to variables within my data object. The variables are locales, so: nl-NL, de-NL and so on.
setMediaForPage : function(data){
if(!this.page.media[this.selectedLanguage]['id'])
{
// set for all locales
var obj = this;
this.application.locales.forEach(function(element, index, array) {
obj.$set(obj.page.media[element.locale], data.media);
})
}
else
{
// set for 1 locale
this.$set(this.page.media[this.selectedLanguage], data.media);
}
}
What happens when I run this code is that the data object shows up properly in Vue Devtools data object, but the media does not show up in the template. When I switch the language (by changing the this.selectedLanguage value), the media does show up.
I think this has to do with the variables in the object keypath, but I'm not 100% sure about that. Any thoughts on how to improve this code so I can show the selected media in the parent component without having to change the this.selectedLanguagevalue?
I don't know your data structure exactly, but you can certainly use variables as the the keypath in vue, however remember that the keyPath should be a string, not an object.
If your variable that you want to use in the keypath is part of the vue, you'd do it like this:
obj.$set('page.media[element.locale]', data.media)
... because the keyPath which is a string is intelligently parsed by Vue's $set method and is of course it knows that this path is relative to the $data object.
new Vue({
el: '#app',
data() {
return {
msg: "hello world",
attr: {
lang: {
zh: '中文',
en: 'english'
}
}
}
},
methods: {
$set2(obj, propertyName, value) {
let arr = propertyName.split('.');
let keyPath = arr.slice(0, -1).join('.');
let key = arr[arr.length - 1];
const bailRE = /[^\w.$]/
function parsePath(obj, path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
let target = parsePath(obj, keyPath);
// console.log(target, key);
// target[key] = value;
this.$set(target, key, value);
}
},
mounted() {
setTimeout(() => {
// this.$set('attr.lang.zh', '嗯');
// this.$set2(this, 'attr.lang.zh', '嗯');
this.$set2(this.attr, 'lang.zh', '嗯');
}, 1000);
}
})
调用示例:this.$set2(this.attr, 'lang.zh', '嗯');
i have also experienced similar problems,remove variables -,these variables nl-NL, de-NL change to nlNl, deNl
and i not use
obj.$set('page.media[element.locale]', data.media)
but
obj.$set('page.media.'+element.locale, data.media);
then it work

React - getting a component from a DOM element for debugging

For the purposes of debugging in the console, is there any mechanism available in React to use a DOM element instance to get the backing React component?
This question has been asked previously in the context of using it in production code. However, my focus is on development builds for the purpose of debugging.
I'm familiar with the Chrome debugging extension for React, however this isn't available in all browsers. Combining the DOM explorer and console it is easy to use the '$0' shortcut to access information about the highlighted DOM element.
I would like to write code something like this in the debugging console:
getComponentFromElement($0).props
Even in a the React development build is there no mechanism to use maybe the element's ReactId to get at the component?
Here's the helper I use: (updated to work for React <16 and 16+)
function FindReact(dom, traverseUp = 0) {
const key = Object.keys(dom).find(key=>{
return key.startsWith("__reactFiber$") // react 17+
|| key.startsWith("__reactInternalInstance$"); // react <17
});
const domFiber = dom[key];
if (domFiber == null) return null;
// react <16
if (domFiber._currentElement) {
let compFiber = domFiber._currentElement._owner;
for (let i = 0; i < traverseUp; i++) {
compFiber = compFiber._currentElement._owner;
}
return compFiber._instance;
}
// react 16+
const GetCompFiber = fiber=>{
//return fiber._debugOwner; // this also works, but is __DEV__ only
let parentFiber = fiber.return;
while (typeof parentFiber.type == "string") {
parentFiber = parentFiber.return;
}
return parentFiber;
};
let compFiber = GetCompFiber(domFiber);
for (let i = 0; i < traverseUp; i++) {
compFiber = GetCompFiber(compFiber);
}
return compFiber.stateNode;
}
Usage:
const someElement = document.getElementById("someElement");
const myComp = FindReact(someElement);
myComp.setState({test1: test2});
Note: This version is longer than the other answers, because it contains code to traverse-up from the component directly wrapping the dom-node. (without this code, the FindReact function would fail for some common cases, as seen below)
Bypassing in-between components
Let's say the component you want to find (MyComp) looks like this:
class MyComp extends Component {
render() {
return (
<InBetweenComp>
<div id="target">Element actually rendered to dom-tree.</div>
</InBetweenComp>
);
}
}
In this case, calling FindReact(target) will (by default) return the InBetweenComp instance instead, since it's the first component ancestor of the dom-element.
To resolve this, increase the traverseUp argument until you find the component you wanted:
const target = document.getElementById("target");
const myComp = FindReact(target, 1); // provide traverse-up distance here
For more details on traversing the React component tree, see here.
Function components
Function components don't have "instances" in the same way classes do, so you can't just modify the FindReact function to return an object with forceUpdate, setState, etc. on it for function components.
That said, you can at least obtain the React-fiber node for that path, containing its props, state, and such. To do so, modify the last line of the FindReact function to just: return compFiber;
Here you go. This supports React 16+
window.findReactComponent = function(el) {
for (const key in el) {
if (key.startsWith('__reactInternalInstance$')) {
const fiberNode = el[key];
return fiberNode && fiberNode.return && fiberNode.return.stateNode;
}
}
return null;
};
I've just read through the docs, and afaik none of the externally-exposed APIs will let you directly go in and find a React component by ID. However, you can update your initial React.render() call and keep the return value somewhere, e.g.:
window.searchRoot = React.render(React.createElement......
You can then reference searchRoot, and look through that directly, or traverse it using the React.addons.TestUtils. e.g. this will give you all the components:
var componentsArray = React.addons.TestUtils.findAllInRenderedTree(window.searchRoot, function() { return true; });
There are several built-in methods for filtering this tree, or you can write your own function to only return components based on some check you write.
More about TestUtils here: https://facebook.github.io/react/docs/test-utils.html
i wrote this small hack to enable access any react component from its dom node
var ReactDOM = require('react-dom');
(function () {
var _render = ReactDOM.render;
ReactDOM.render = function () {
return arguments[1].react = _render.apply(this, arguments);
};
})();
then you can access any component directly using:
document.getElementById("lol").react
or using JQuery
$("#lol").get(0).react
In case someone is struggling like me to access React component/properties from a chrome extension, all of the above solutions are not going to work from chrome extension content-script. Rather, you'll have to inject a script tag and run your code from there. Here is complete explanation:
https://stackoverflow.com/a/9517879/2037323
Here is a small snippet i'm currently using.
It works with React 0.14.7.
Gist with the code
let searchRoot = ReactDom.render(ROOT, document.getElementById('main'));
var getComponent = (comp) => comp._renderedComponent ? getComponent(comp._renderedComponent) : comp;
var getComponentById = (id)=> {
var comp = searchRoot._reactInternalInstance;
var path = id.substr(1).split('.').map(a=> '.' + a);
if (comp._rootNodeID !== path.shift()) throw 'Unknown root';
while (path.length > 0) {
comp = getComponent(comp)._renderedChildren[path.shift()];
}
return comp._instance;
};
window.$r = (node)=> getComponentById(node.getAttribute('data-reactid'))
to run it, open Devtools, highlight an element you want to examine, and in the console type : $r($0)
I've adapted #Venryx's answer with a slightly adapted ES6 version that fit my needs. This helper function returns the current element instead of the _owner._instance property.
getReactDomComponent(dom) {
const internalInstance = dom[Object.keys(dom).find(key =>
key.startsWith('__reactInternalInstance$'))];
if (!internalInstance) return null;
return internalInstance._currentElement;
}
React 16+ version:
If you want the nearest React component instance that the selected DOM element belongs to, here's how you can find it (modified from #Guan-Gui's solution):
window.getComponentFromElement = function(el) {
for (const key in el) {
if (key.startsWith('__reactInternalInstance$')) {
const fiberNode = el[key];
return fiberNode && fiberNode._debugOwner && fiberNode._debugOwner.stateNode;
}
}
return null;
};
They trick here is to use the _debugOwner property, which is a reference to the FiberNode of the nearest component that the DOM element is part of.
Caveat: Only running in dev mode will the components have the _debugOwner property. This would not work in production mode.
Bonus
I created this handy snippet that you can run in your console so that you can click on any element and get the React component instance it belongs to.
document.addEventListener('click', function(event) {
const el = event.target;
for (const key in el) {
if (key.startsWith('__reactInternalInstance$')) {
const fiberNode = el[key];
const component = fiberNode && fiberNode._debugOwner;
if (component) {
console.log(component.type.displayName || component.type.name);
window.$r = component.stateNode;
}
return;
}
}
});
Install React devtools and use following, to access react element of corresponding dom node ($0).
for 0.14.8
var findReactNode = (node) =>Object.values(__REACT_DEVTOOLS_GLOBAL_HOOK__.helpers)[0]
.getReactElementFromNative(node)
._currentElement;
findReactNode($0);
Ofcourse, its a hack only..

Categories

Resources