Altering the DOM manually in React - javascript

I am using Ionic's IonPicker component, which renders, as a sibling to React's div#root, an ion-picker element, which appears to be a full screen overlay, which has two children: an ion-backdrop element, and its sibling, div.picker-wrapper.
I am trying to extract just the div.picker-wrapper element so that I can, say, place it relative to other components in my app, and I also want to be able to add elements to it, like a button that is a child of div.picker-wrapper, and that button would have to be a react component. So I suppose individual access to div.picker-wrapper's existing children, div.picker-toolbar and div.picker-columns, would be good as well.
And of course I need to be able to use the IonPicker's props and methods and everything.
So I'm trying to grab DOM elements and delete them, move them around, stuff like that, and of course I don't ever want the user to see things disappearing and reappearing, so, there's that.
I imagine the answer involves "Use refs" so please provide more detail. To start, I've tried
picker: RefObject<HTMLIonPickerElement> = React.createRef();
render() {
return (
<IonPicker
isOpen
ref={ this.picker }
animated={ true }
mode={ 'ios' }
cssClass={ 'firmm-picker' }
columns={ this.columns }
onDidDismiss={ this.props.dismiss }
buttons={ this.buttons }
showBackdrop={ false }
/>
);
}
But picker and/or picker.current are always undefined or null.
And I've tried const x = document.getElementById('ion-overlay-1') in various places (componentDidMount, render, shouldComponentUpdate), and it's never there.

Related

How to preserve children state when changing parents in react?

I am new to React and working on an app that changes navbar layout and some other details according to screen size, but renders the same responsive content inside. The <MyContent /> itself can vary according to the route or tab the user is visiting.
function App() {
const isMobile = useSomeHookToDetectIfMobile();
return (
{isMobile
? <MobileWrapper><MyContent /></MobileWrapper>
: <DesktopWrapper><MyContent /></DesktopWrapper>
}
);
}
The thing is, if the user resizes the window, when changing layout all the state inside <MyContent /> is lost and reset. What would be the most React way to maintain it on resize? Keep in mind that the shape of the state of <MyContent /> can vary according to what components it is displaying, so I don't think lifting state up is the way to go. Also its state only concerns itself, so I don't think it belongs farther up the component chain. I think I am missing some key point about the problem or coming at it the wrong way. Any help would be appreciated.
In this case, a simple local variable to keep the rendered content before the condition would be enough. That way, React can keep track of it and then it's always the same instance.
function App() {
const isMobile = useSomeHookToDetectIfMobile();
const content = <MyContent />;
return (
{isMobile
? <MobileWrapper>{content}</MobileWrapper>
: <DesktopWrapper>{content}</DesktopWrapper>
}
);
}

How do we add custom data-attributes to Gutenburg's editor.BlockListBlock?

For Gutenburg's "core/image" block, they have different alignment options, specifically two that expose "full" and "wide". When you click on either one of these options, you will see that the data attribute "data-align" is added to the block editor's wrapper component with a value of either "full" or "wide".
I'm trying to create a custom block that has a similar features as the above. But I'm having a hard time figuring out how to add that custom attribute to my component's block editor wrapper.
Some things that I've tried are:
Using the block filter editor.BlockListBlock but the most I could do with this with my own knowledge is adjust the props, and the className. Adding a data-align="full" just meant adding a prop called data-alignment here.
https://developer.wordpress.org/block-editor/developers/filters/block-filters/#editor-blocklistblock
I also tried doing this with jQuery. Even if this worked, I definitely didn't want to use it as a permanent solution, I just wanted to see if it would work. So I added an on('click') event to one of my buttons so that it would target the wrapper component and modify the node, but that didn't work either. Probably because the block element is a dynamic element, it couldn't even be selected.
This is the wrapper that I'm trying to add a custom attribute to,
<div id="block-388288fa-ff20-459e-bce7-543b94fd77c4" class="wp-block editor-block-list__block block-editor-block-list__block is-selected" data-type="cgb/block-ee-hero-slider" tabindex="0" aria-label="Block: Hero Slider">
I just ran into the same problem. I found two solutions.
With getEditWrapperProps()
If you define your block yourself through registerBlockType(), then you can use getEditWrapperProps to define the data-align attribute:
registerBlockType('my-fully-aligned-block', {
title: 'My Fully Aligned Block',
category: 'common',
icon: 'admin-appearance',
/**
* Sets alignment.
*
* #param attributes
* #returns {{'data-align': *}}
*/
getEditWrapperProps(attributes) {
return {
'data-align': 'full'
};
},
edit,
save: () => null
});
With the editor.BlockListBlock filter
If you want to change the alignment for an existing block, you can use the editor.BlockListBlock filter that you already tried. Instead of setting the className property, like in the example in the documentation, you can pass in wrapperProps, that is going to be merged with what is defined in getEditWrapperProps().
function FullAlign(BlockListBlock) {
return props => {
const { block } = props;
// Bail out if it’s not the block we want to target.
if ('cgb/block-ee-hero-slider' !== block.name) {
return <BlockListBlock {...props} />;
}
return (
<BlockListBlock {...props} wrapperProps={{ 'data-align': 'full' }} />
);
};
}
wp.hooks.addFilter(
'editor.BlockListBlock',
'cgb/block-ee-hero-slider',
FullAlign
);

How do you reliably measure React components for computed position of popovers and callouts?

I'm using react and react-bootstrap. I want to know whether there's a better, cleaner, more idiomatic approach than what I've come up with.
The Problem
I want to position a popover callout that points to a button on my page that provides a hint for where the user should start their workflow. The user can subsequently dismiss this popover. It took me a long time to figure out this hack-y approach to measure the elements' dimensions and positions to properly position the callout:
The target of the callout:
<Button
bsClass="button"
className="button-blue"
id="connectParentsButton"
onClick={( event ) => { this.handleOpenInviteParents( event );}}
ref={( input ) => { this.connectParentsButtonRef = input; }}
>
+ Connect Parents
</Button>
The idea is to use ref to store the target as a property of the parent React component, and then access it from the sibling callout component to perform the measurements.
The callout component:
<RightBottomHintBox
isShow={this.state.isShowConnectParentsHint && !this.state.isDismissHideConnectParentsHint}
target={this.connectParentsButtonRef}
id="connectParentsHint"
onHide={this.handleHideConnectParentsHint}
/>
In my RightBottomHintBox component, I have to
use document.getElementById() to get dimenions and positions
use guards to avoid trying to access nodes that don't exist yet
store dimensions and coordinates in class properties rather than component state
And I have to do this in the callout's render() to ensure that my callout component gets mounted even if it's not displayed, so that the DOM node exists and can be correctly measured.
let myStyle = this.props.isShow ? {} : { visibility: 'hidden' };
Failed approaches
computing dimensions when the callout's componentDidMount() fires. The DOM nodes of either the callout or the target are often not yet there
computing dimensions when the parent component's componentDidMount() fires. The same problem exists, you can't be sure that the DOM nodes exist at this point.
The above two approaches led me to this thread: When exactly is `componentDidMount` fired?
One post suggests that to be sure that all DOM nodes are ready, you'd have to go up to the top component. I don't like this approach because it would break encapsulation and separation of concerns.
Another thread, How can I respond to the width of an auto-sized DOM element in React?, suggests using the react-measure library to do measurements, which might work, but for my particular case, I wanted to avoid having another library to manage because this app is small.
Background context
I'm creating a page that allows the user to input a list of students' parents' email addresses. The initial page lists students and parents' email addresses that have already been input. However, the first time the user comes to this page, there may be a list of students, with no parent email addresses. I want to provide a hint to the user that they should click on 'Connect Parents' button to open up a wizard to guide the user in data input.
I have another failed attempt here that may spur someone to the correct solution.
export function getSize(cpt) {
var elem = document.getElementById("ruler");
const html = ReactDOMServer.renderToStaticMarkup(cpt);
elem.innerHTML = html;
return [elem.scrollWidth, elem.scrollHeight];
}
var Foo = React.createClass({
componentDidMount() {
const sz = this.elem.getBoundingClientRect();
ReactDOM.render(<span>{ [sz.width,sz.height].join("x") }</span>,
document.getElementById('actual'));
},
render() {
return <div ref={e => { this.elem = e; }} className="content">
<p> Bar!! BAZ </p> <p> another line </p> </div>;
}
})
Setting up getSize to render into any div (onscreen or off) gives about 76x84, while the actual size reported by componentDidMount (and confirmed by dev tools) is 188x52.

Show/Hide ReactJS components without losing their internal state?

I've been hiding/showing react components by not rendering them, for example:
render: function() {
var partial;
if (this.state.currentPage === 'home') {
partial = <Home />;
} else if (this.state.currentPage === 'bio') {
partial = <Bio />;
} else {
partial = <h1>Not found</h1>
}
return (
<div>
<div>I am a menu that stays here</div>
Home Bio
{partial}
</div>
);
}
but just say that the <Bio/> component has lots of internal state. Everytime I recreate the component, it loses it's internal state, and resets to it's original state.
I know of course that I could store the data for it somewhere, and pass it in via props or just globally access it, but this data doesn't really need to live outside of the component. I could also hide/show components using CSS (display:none), but I'd prefer to hide/show them as above.
What's the best practice here?
EDIT: Maybe a better way to state the problem is to use an example:
Ignore React, and assume you were just using a desktop app that had a configuration dialog with a Tab component called A, which has 2 tabs, named 1 and 2.
Say that tab A.1 has an email text field and you fill in your email address. Then you click on Tab A.2 for a second, then click back to Tab A.1. What's happened? Your email address wouldn't be there anymore, it would've been reset to nothing because the internal state wasn't stored anywhere.
Internalizing the state works as suggested in one of the answers below, but only for the component and it's immediate children. If you had components arbitrarily nested in other components, say Tabs in Tabs in Tabs, the only way for them to keep their internal state around is to either externalize it somewhere, or use the display:none approach which actually keeps all the child components around at all times.
It just seems to me that this type of data isn't data you want dirtying up your app state... or even want to even have to think about. It seems like data you should be able to control at a parent component level, and choose to either keep or discard, without using the display:none approach and without concerning yourself with details on how it's stored.
One option would be to move the conditional inside the component itself:
Bio = React.createClass({
render: function() {
if(this.props.show) {
return <p>bio comp</p>
} else {
return null;
}
}
});
<Bio show={isBioPage} />
Whether this is "best practise" or not probably depends on the exact situation.
Unfortunately, style={{display: 'none'}} trick only works on normal DOM element, not React component. I have to wrap component inside a div. So I don't have to cascade the state to subcomponent.
<div className="content">
<div className={this.state.curTab == 'securities' ? 'active' : ''}>
<Securities />
</div>
<div className={this.state.curTab == 'plugins' ? 'active' : ''}>
<Plugins />
</div>
</div>
Looks like official documentation suggests hiding stateful children with style={{display: 'none'}}
The fundamental problem here is that in React you're only allowed to mount component to its parent, which is not always the desired behavior. But how to address this issue?
I propose the solution, addressed to fix this issue. More detailed problem definition, src and examples can be found here: https://github.com/fckt/react-layer-stack#rationale
Rationale
react/react-dom comes comes with 2 basic assumptions/ideas:
every UI is hierarchical naturally. This why we have the idea of components which wrap each other
react-dom mounts (physically) child component to its parent DOM node by default
The problem is that sometimes the second property isn't what you want
in your case. Sometimes you want to mount your component into
different physical DOM node and hold logical connection between
parent and child at the same time.
Canonical example is Tooltip-like component: at some point of
development process you could find that you need to add some
description for your UI element: it'll render in fixed layer and
should know its coordinates (which are that UI element coord or
mouse coords) and at the same time it needs information whether it
needs to be shown right now or not, its content and some context from
parent components. This example shows that sometimes logical hierarchy
isn't match with the physical DOM hierarchy.
Take a look at https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example to see the concrete example which is answer to your question (take a look at the "use" property):
import { Layer, LayerContext } from 'react-layer-stack'
// ... for each `object` in array of `objects`
const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
return (
<Cell {...props}>
// the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
<Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
hideMe, // alias for `hide(modalId)`
index } // useful to know to set zIndex, for example
, e) => // access to the arguments (click event data in this example)
<Modal onClick={ hideMe } zIndex={(index + 1) * 1000}>
<ConfirmationDialog
title={ 'Delete' }
message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
confirmButton={ <Button type="primary">DELETE</Button> }
onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hideMe) } // hide after confirmation
close={ hideMe } />
</Modal> }
</Layer>
// this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
<LayerContext id={ modalId }> {({showMe}) => // showMe is alias for `show(modalId)`
<div style={styles.iconOverlay} onClick={ (e) => showMe(e) }> // additional arguments can be passed (like event)
<Icon type="trash" />
</div> }
</LayerContext>
</Cell>)
// ...

Reactjs append an element instead of replacing

I'm trying to iterate through a list/array/object of things: (I used coffeescript to keep it clear, jsfiddle of full JS here. but it's just a forEach)
pages = for page, each of #props.ids
$('#start').append("<div id='"+page+"' ></div>")
React.renderComponent page(title: each.title, text: each.text), $("#"+page)[0]
and append each of them, instead of replacing, leaving only the last item in the list.
Where the #start element is the starting container, and I want to populate it with multiple elements, but I need to give each their own container, otherwise they will all overwrite eachother, the default reactjs behaviour.
I'm wondering if there's a way to tell react to append instead of replacing the div.
I'd like to avoid using jquery if possible, and do it purely react-ly.
I though about giving the React.renderComponent page the initial list, and then iterate in the previously called template, however, then i'm facing a different problem, I have to return a reactjs element object, consisting of the whole list, which I really don't prefer.
I need for the initial call to create individual, independent react templates, appending eachother in a list, prefferably without any extra containers, or using jquery.
I think you're getting the concept wrong. React's renderComponent indeed renders a single component, somewhere. Doing this multiple times only re-renders the same component at that place (aka idempotent). There's no real "append" statement, at least not in the way you asked for.
Here's an example of what you're trying to achieve. Forgot about renderComponent in this. It's just to put the component somewhere.
/** #jsx React.DOM */
var pages = [{title: 'a', text: 'hello'}, {title: 'b', text: 'world'}];
var App = React.createClass({
render: function() {
return (
<div>
{
this.props.pages.map(function(page) {
return <div>Title: {page.title}. Text: {page.text}</div>;
})
}
</div>
);
}
});
React.renderComponent(<App pages={pages} />, whateverDOMNodeYouWantItToBeOn);
See what I did there? If I want multiple divs, I just create as many as I want to see. They represent the final look of your app, so making a same div be "appended" multiple times doesn't really make sense here.
Create a div with a class extradiv:
<div class="extradiv">
</div>
In CSS, Set It's display to none:
.extradiv {
display: none;
}
.extradiv * {
display: none;
}
In JS implement this function:
function GoodRender(thing, place) {
let extradiv = document.getElementsByClassName('extradiv')[0];
ReactDOM.render(thing, extradiv);
extradiv = document.getElementsByClassName('extradiv')[0];
place.innerHTML += extradiv.innerHTML;
}
Than you can use this in place of ReactDOM.render:
GoodRender(<Component>My Text</Component>, YourDOMObject)

Categories

Resources