Adding "manually" multiple React components in the same container - javascript

I'm migrating an old "jQuery" application, adding some React components. I have some JS/jQuery code adding some elements in the DOM. I want to replace this using for example a new Item component, creating many instances and adding them to the same container. And I need to get the real DOM element to manipulate it (with the old JS/jQuery code).
I found this solution :
const elt1 = ReactDOM.findDOMNode(ReactDOM.render(<CalendarItem item={item} />, container))
but the container content is replaced with the new Item and adding many items, only the last is finally in the container.
I have tried portal :
const elt2 = ReactDOM.createPortal(<CalendarItem item={item} />, container)
but the returned element elt2 is not a DOM element (that I can manipulate after).
Is there a solution to do this ?
Thanks

If you want to manipulate React components using their related DOM elements, look into Refs (https://reactjs.org/docs/refs-and-the-dom.html).
So create the ref in the constructor:
constructor(props) {
super(props);
...
this.calendarItem = React.createRef();
}
And then give the ref to your new CalendarItem:
<CalendarItem item={item} ref={this.calendarItem} />
You can then access the actual DOM node of that CalendarItem using this.calendarItem.current, for example:
$(this.calendarItem.current).focus();

Related

Web Components - Multiple elements per slot

I'm working on new web-components and ran into some issues concerning slots.
I have an outer container-component with a "main-slot" inside, in which multiple elements should be inserted (into the same slot). However, it is only possible to add one element per named slot.
My question: is there a way to add multiple elements to one named slot? Like shown here:
<own-container>
<own-element slot="main"></own-element>
<own-element slot="main"></own-element>
<own-element slot="main"></own-element>
<own-element slot="main"></own-element>
</own-container>
There is also imperative <slot>
super().attachShadow({
mode: 'open',
slotAssignment: 'manual' // imperative assign only
})
But! you get Named slots OR Imperative slots, on one Web Component
To mimic named <slot>, assigning content to the same named <slot>
you probably need the Mutation Observer API
addendum
You can have multiple elements per slot:
<component-with-slots>
<H1 slot="title">Web Components are great!</H1>
<h2 slot="title">A bit hard to master</h2>
<b slot="title">But Great!</b>
</component-with-slots>
<script>
customElements.define('component-with-slots', class extends HTMLElement {
constructor() {
super()
.attachShadow({mode:'open'})
.innerHTML="<slot name='title'></slot>";
}
});
</script>
Nope. It is not possible for named slot. The trick is to have a wrapper div element to wrap your lightDOM.
<own-container>
<div slot="main">
<own-element></own-element>
<own-element></own-element>
<own-element></own-element>
<own-element></own-element>
</div>
</own-container>
If the presence of additional div causes styling problem, then you can use new contents type of display box.
div {
display: contents;
}
The display: contents causes an element's children to appear as if they were direct children of the element's parent, ignoring the element itself. However, note that it can cause accessibility issues.

How can we append childnodes to ng-content without rerendering the Angular Elements?

The child nodes which Iam appending are not properly positioned inside the ng-content area of the custom element I created using Angular element.
I tried to reporduce my issue in this Stackblitz
Current solution
It will work if I add the child nodes to the created custom element instance before adding to dom .
Expected Solution
Is there any way I can easily add a childnode to my custom angular
element using javascript(appendChild) without rerendering the whole
component again.
Thanks.
You could use getElementsByClassName:
onAddChild2(e: string) {
let div = document.createElement("div");
div.innerHTML = e;
const eHost = document.getElementsByClassName('container')[0];
eHost.appendChild(div);
}
Check the Stackblitz I forked from you code: https://stackblitz.com/edit/angular-elements-exp-v3r4b3
This is not the Angular way. You should not be manipulating the DOM directly like this. You should be binding an array to your template.
children = ['New Child'];
onAddChild() {
this.children.push('New child');
}
and in the template
<div *ngFor="let child of children">{{child}}</div>

Passing only clicked element to onClick function - reactjs

I have multiple elements with same className and i want if some element (with className history-node) is clicked it should get className active along with current className.
But i am having an issue, there are childs of that element and if child elements gets clicked they also get class Active.
Here is the code:
<div className="history-node-container" key={index}>
<div className="history-node" onClick={(e) => {this.handleHistoryClick(e.target)}}>
<span className="history-title">{heading.action}</span>
<span className="history-date">{moment(heading.added_at).format("MMMM Do, YYYY")}</span>
</div>
</div>
handleHistoryClick function
handleHistoryClick(target){
$('.history-node').removeClass('active'); //removing active class from all elements
target.className = 'history-node active';
}
I want to run function when user click on element with className history-node
But if user clicks on history-title, ClickHandler gives class active to history-title element.
EXPECTED BEHAVIOUR: if history-node is clicked only history-node should get class active.
One tip and how to solve your problem (In two ways)
Tip: Not usually the best idea to mix React with jQuery. React came in as a major paradigm shift in how we interact with the DOM. Try to read a little bit more about React, how it works, why is it so different from simply adding/removing elements in the DOM with jQ.
Some references to help you with that:
How to go from jQuery to React.js?
Thinking in React for jQuery Programmers
Now, back to your question
You should use currentTarget.
As the .history-title and .history-date elements are wrapped within .history-node, any click on them will trigger it's parent's event, since .history-node body is .history-title + .history-date. That's the correct behavior. When you trigger an event in JS, the event object receives two parameters: target, which is the event triggering the event and currentTarget, the element that is actually listening for the event.
Now for the code:
with JQ
Component:
<div className="history-node-container" key={index}>
<div className="history-node" onClick={handleHistoryClick}>
<span className="history-title">{heading.action}</span>
<span className="history-date">{moment(heading.added_at).format("MMMM Do, YYYY")}</span>
</div>
</div>
Click:
handleHistoryClick(event){
$('.history-node').removeClass('active')
event.currentTarget.classList.add('active')
}
The React way (Pure React, no modules)
Component:
class HistoryNode extends Component {
constructor(props) {
super(props)
this.state = { isActive: false }
this.handleClick = this.handleClick.bind(this)
}
handleClick(e) {
let state = this.state
this.setState({isActive: !state.isActive})
}
render() {
return(
<div className="history-node-container">
<div className={`history-node ${this.state.isActive ? 'active' : ''}`} onClick={handleHistoryClick}>
<span className="history-title">{heading.action}</span>
<span className="history-date">
{moment(heading.added_at).format("MMMM Do, YYYY")}</span>
</div>
</div>
)
}
}
Notice how you don't need to manipulate the DOM at any moment at the React solution. You just attach your event to that particular component, define how it's state change should reflect on the UI and let React do the rest.
Hope it helps ;)
Reference for target vs currentTarget
I think the event propagates to child components.
Have you tried this ?
<div className="history-node-container" key={index}>
<div className="history-node" onClick={handleHistoryClick}>
<span className="history-title">{heading.action}</span>
<span className="history-date">{moment(heading.added_at).format("MMMM Do, YYYY")}</span>
</div>
</div>
HandleClick function
handleHistoryClick(event){
event.stopPropagation();
$('.history-node').removeClass('active');
event.target.className = 'history-node active';
}
EDIT : You could make it simpler though (and without jQuery) using your component state. But without knowing how you wrote your component I cannot give you a snippet illustrating it. Be careful too as you interact directly with the DOM, this implies a performance loss. Using the React state allows you to avoid such thing!

Failed to execute removeChild on Node

Other stack answers such as this and this seem to be specialized cases and I believe my case is more generalized. I am doing this in my js:
var markerDiv = document.createElement("div");
markerDiv.innerHTML = "<div id='MyCoolDiv' style='color: #2b0808'>123</div>";
document.getElementById("playerContainer").appendChild(markerDiv);
// after a brief delay, REMOVE the appended child
setTimeout(function(){
var myCoolDiv = document.getElementById("MyCoolDiv");
document.getElementById("playerContainer").removeChild(myCoolDiv);
}, 1500);
Everything works correctly and as expected (the div is correctly appended and I can see it) until removeChild() is called, at which time I get the error Failed to execute 'removeChild' on 'Node'.
What am I doing wrong?
Your myCoolDiv element isn't a child of the player container. It's a child of the div you created as a wrapper for it (markerDiv in the first part of the code). Which is why it fails, removeChild only removes children, not descendants.
You'd want to remove that wrapper div, or not add it at all.
Here's the "not adding it at all" option:
var markerDiv = document.createElement("div");
markerDiv.innerHTML = "<div id='MyCoolDiv' style='color: #2b0808'>123</div>";
document.getElementById("playerContainer").appendChild(markerDiv.firstChild);
// -------------------------------------------------------------^^^^^^^^^^^
setTimeout(function(){
var myCoolDiv = document.getElementById("MyCoolDiv");
document.getElementById("playerContainer").removeChild(myCoolDiv);
}, 1500);
<div id="playerContainer"></div>
Or without using the wrapper (although it's quite handy for parsing that HTML):
var myCoolDiv = document.createElement("div");
// Don't reall need this: myCoolDiv.id = "MyCoolDiv";
myCoolDiv.style.color = "#2b0808";
myCoolDiv.appendChild(
document.createTextNode("123")
);
document.getElementById("playerContainer").appendChild(myCoolDiv);
setTimeout(function(){
// No need for this, we already have it from the above:
// var myCoolDiv = document.getElementById("MyCoolDiv");
document.getElementById("playerContainer").removeChild(myCoolDiv);
}, 1500);
<div id="playerContainer"></div>
For me, a hint to wrap the troubled element in another HTML tag helped. However I also needed to add a key to that HTML tag. For example:
// Didn't work
<div>
<TroubledComponent/>
</div>
// Worked
<div key='uniqueKey'>
<TroubledComponent/>
</div>
The direct parent of your child is markerDiv, so you should call remove from markerDiv as so:
markerDiv.removeChild(myCoolDiv);
Alternatively, you may want to remove markerNode. Since that node was appended directly to videoContainer, it can be removed with:
document.getElementById("playerContainer").removeChild(markerDiv);
Now, the easiest general way to remove a node, if you are absolutely confident that you did insert it into the DOM, is this:
markerDiv.parentNode.removeChild(markerDiv);
This works for any node (just replace markerDiv with a different node), and finds the parent of the node directly in order to call remove from it. If you are unsure if you added it, double check if the parentNode is non-null before calling removeChild.
I was wraped it with <> </> as a parent when I changed it to normal , div , its worked fine
As others have mentioned, myCoolDiv is a child of markerDiv not playerContainer. If you want to remove myCoolDiv but keep markerDiv for some reason you can do the following
myCoolDiv.parentNode.removeChild(myCoolDiv);
JSFiddle
I'd a similar problem in the vue.js project. Then, I got a hint on changing the fragment wrapper to an HTML element. The most common use case for fragments is probably when you need to return multiple elements. With fragments this is easy and you don't need your typical wrapper div for the elements. Its short syntax is <></>.
Basically, I used the fragment pattern in Vue then I got the above error rendering the component dynamically using transition. It appears that the dynamic component(which entails multiple elements) needed to be wrapped with an HTML element, not a fragment.
// Vuejs
<transition
name="router-anim"
enter-active-class="animated animated-enter"
mode="out-in"
leave-active-class="animated animated-exit"
>
<router-view /> // dynamic rendering based on current route using vue-router
</transition>

How to target classes with queryselector when using react-css-modules

React-css-modules makes up class names on the fly to limit name clashes while using standalone components. Thats awesome. But once the DOM is loaded and you need to target a class for an animation, for instance, the class names that css-modules comes up with are, in parts, randomized.
How to go about with this?
According from react-css-modules official doc, you can target class like bellow
render() {
const animated = this.props.styles['animated']
return <div className={animated}>something</div>
}
Query selector shouldn't be necessary with react just attach a ref to the element you want and you have access to it within any function of the react component.
https://facebook.github.io/react/docs/more-about-refs.html
So I ended up using the classNames npm module
to add and remove classes to an element when I need more than one. Works like this:
let myClasses = classNames({
'button': true,
'special': this.state.special
})
<button className={myClasses} />

Categories

Resources