Vue3 web component set attribute - javascript

I created stand alone web component, with Vite and Vue3, to replace old jQuery libraries.
When rendered in html, it looks something like:
<custom-datepicker id="deliveryTime">
#shadow-root
<div>...actual component...</div>
</custom-datepicker>
When user select date it would be perfect if I can set attribute value to this element, like:
<custom-datepicker value="selected value HERE" ...
so the form can then use this value from the element by it's id.
Problem is that the only way I manage to achieve this is by emitting event (selected) from my web component and listening to that event, so the form can use value, like:
const datepicker = document.querySelector('#deliveryTime');
const dateSelected = (e) => {
datepicker.value = e.detail.val;
}
window.addEventListener('selected', dateSelected);
If I set value attribute within web component, rest of the app (Apache Velocity template) can't access it because it is in the shadow-root:
<custom-datepicker id="deliveryTime">
#shadow-root
<div value="selected value is sadly HERE">...actual component...</div>
</custom-datepicker>
Is it even possible to achieve this from inside the web component (so I can omit listener)?

I found solution, maybe not the cleanest one but it works :)
In my web-component, in mounted hook component is selected:
const dp = ref(null)
onMounted(() => {
dp.value = document.querySelector("#deliveryTime");
})
After date is selected, instead of emitting just set attribute:
dp.value.setAttribute('value', value)

What if you access the HTMLElement linked to your shadowhost and then use the "setAttribute" function on it ? Assuming you have access to the shadowRoot inside your component using 'this', I think something like that will do the trick (https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/host) :
const shadowHost = this.shadowRoot.host; // The HTMLElement linked to <custom-datepicker />
shadowHost.setAttribute("value", someValueOfYourChoice);

Related

How to generate UID that will persist in both server and client side in nuxt?

Maybe the heading is misleading, so I described my issue as thoroughly as I could.
I have a class that generates some id for itself.
class Div {
id = null
constructor() {
// some more data
this.id = id()
}
onMounted() {
const HTMLElement = document.getElementById(this.id)
HTMLElement.style.setProperty('--custom-property', 'lol')
// do some stuff
}
}
Then I render it on server-side (sorry for Pug):
<template lang="pug">
.divs
div(
v-for="div in divs"
:id="div.id"
) {{ div.data }}
</template>
After in onMounted hook I want to apply some dynamic generated stuff like styles or custom properties (as you can see above):
setup() {
const divs = [
new Div(),
new Div(),
new Div()
]
onMounted(() => {
divs.forEach(div => div.onMounted())
})
return {
divs
}
}
The problem is that the actual id's in array and in the DOM are different, so I got undefined while tring to call getElementById().
The reason why I am doing this is that I am creating a plugin and need these blocks to be rendered on server-side (by obvious reasons), but I need to set some custom properties because they will be provided by the end user. I don't think there is an other way to solve this, but I am open to any suggestions. Maybe it's a common issue that is solved already?
I also have no idea why id's are different, because I though that classes instantiated once on server, but it seems like they do it twice.

ReactJS - render external DOM element with events

I use ReactJS 16.13.1 and now I want to render an external DOM element with its events.
So let's assume there is a
<button type="button" id="testBtnSiri" onclick="alert('Functionality exists');">Testbutton Siri</button>
Which has been generated by a library (with some other events as well).
Now I want:
To copy it (with events) & render this element in my ReactJS render function.
What is the most appropriate way to do this ?
I am not looking for ReactDOM.createPortal().
I want to show an external DOM element with its events in my React Component.
In order to use HTML within your React code you need to make use of dangerouslySetInnelHTML prop on an element.
function libraryReturnedHTML() {
return '<button type="button" id="testBtnSiri" onclick="alert('Functionality exists');">Testbutton Siri</button>'
}
function MyComponent() {
return <div dangerouslySetInnerHTML={__html: libraryReturnedHTML} />;
}
However you must know that this makes your code susceptible to XSS attacks. In order to prevent such scenarios its a good idea to first sanitize your HTML so that you have only the desired scripts and link tags. In order to sanitizehtml you can simply use normal Javascript functions and document.createTreeWalker
You can also make use of an existing library to sanitize your HTML. For example sanitize-html
However the sanitization is just an added precaution.
Create a wrapper over Node.addEventListener() which will catch all the event listeners added to the button when it's rendered by other library. It could look like this(this code have to be executed before the external library creates the button):
let ael = Node.prototype.addEventListener
let eventListeners = []
Node.prototype.addEventListener = function(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void {
if((this as HTMLElement).id === "buttonID")
eventListeners.push({type, listener, options})
this.addEventListener = ael
this.addEventListener(type, listener, options)
}
Create react component with your button - you can create a new one, or inject using dangerouslySetInnerHtml. Pass eventListeners array as a prop. On component mount and unmount, add/remove the event listeners to your button.
function ClonedButton({eventListeners}: any){
useEffect(()=>{
eventListeners.forEach((el) => document.getElementById("my-migrated-new-button")!.addEventListener(el.type, el.listener, el.options));
return () => {
eventListeners.forEach((el) => document.getElementById("my-migrated-new-button")!.removeEventListener(el.type, el.listener, el.options));
}
}, [])
return <button id="my-migrated-new-button"></button>
}

Changing class state from an imported function in ReactJS

I am trying to change the state of a class from an imported named component so I only need to write the code once. Is this even possible?
I have 3 files ( Login, ForgotPassword, Register ). On ALL of these files I am listening for an "onChange" event on the input fields that all do the same thing as show below:
onChange method:
onChange = (e) => {
this.setState(() => ({ errors: {} }));
let fields = this.state.fields;
fields[e.target.name] = e.target.value;
this.setState(() => {
return {
fields
}
});
};
I'd really like to import this as a named method along with some others that are working already:
import { onFocus, onBlur, onChange } from './Utils/Input';
The issue is (as seen above in the onChange code), that I need to update the class state from this method.
Is this at all possible? I am very new to React, so I might be going about this in the completely wrong way.
Thank you in advance!
When trying to update the state of a component you are always updating the state regarding to a particular this.
You are trying to write on* functions which are independent of the instances you are working with. So it makes no sense to use "this" inside those.
You could pass the instance of the Class to the on-function like this:
https://codesandbox.io/s/focused-cache-zzfvp
Use the method by binding the context to it from inside the class like this
onChange.bind(this)

Trigger onChange when setting State in componentDidMount?

I need to add some query paramaters to my url as a person checks off checkboxes.
I am using react router so I do something like this in my checkboxes on change event.
const stringified = queryString.stringify(parsed);
const path = `${this.props.location.pathname}?${stringified}`;
this.props.history.replace(path)
This does however seem to cause a re-render of the page(not sure if this should be happening, would prefer it not to do that so maybe I got to use something other than replace?).
I wanted to check on componentDidMount the url to see if the value is there. If it is there then I wanted to update the state of the checkbox.
#observable
isChecked = false;
#action
componentDidMount() {
const parsed = queryString.parse(this.props.location.search);
this.isChecked = parsed && parsed["param"] === this.props.option;
}
However I don't see the onChange being trigger.
Right now I have on change a function that takes the value and uses it to filter, so I need the function to run.
I could put that function in the componentDidMount but I wanted to make sure before I do that, there is nothing I am missing on why the change event is not be fired.
Try setting the state in the constructor() and incomponentDidUpdate().
When a URL parameter is added to the same route, the existing component is utilized (i.e. an update event) vs. a new one being created. As a result, you won't see a componentDidMount() event.
Another option/solution is to update the state of isChecked directly and push the history/url change.
If what you are trying to prevent is the page refresh use this built in function in your onSubmit event(if I understand your question correctly.)
event.preventDefault();
It stops the browser from auto-refreshing! Make sure to call event in your function though.
ie
onSubmit=(event)=>{
event.preventDefault();
//rest of code
}
If you are trying to filter, the es6 .filter method is useful for checkboxes. I personally used a select dropdown menu to filter the options and selectively show the ticked items in a ToDo List: "Done" "Active" "Completed" and used those states in my filter method.

React DnD: Avoid using findDOMNode

I don't fully understand it but apparently it isn't recommended to use findDOMNode().
I'm trying to create drag and drop component but I'm not sure how I should access refs from the component variable. This is an example of what I currently have:
const cardTarget = {
hover(props, monitor, component) {
...
// Determine rectangle on screen
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
...
}
}
Source
Edit
It might be caused by my component being both the drag and drop source and target as I can get it to work in this example but not this one.
Assuming you're using es6 class syntax and the most recent version of React (15, at time of writing), you can attach a callback ref like Dan did in his example on the link you shared. From the docs:
When the ref attribute is used on an HTML element, the ref callback receives the underlying DOM element as its argument. For example, this code uses the ref callback to store a reference to a DOM node:
<h3
className="widget"
onMouseOver={ this.handleHover.bind( this ) }
ref={node => this.node = node}
>
Then you can access the node just like we used to do with our old friends findDOMNode() or getDOMNode():
handleHover() {
const rect = this.node.getBoundingClientRect(); // Your DOM node
this.setState({ rect });
}
In action:
https://jsfiddle.net/ftub8ro6/
Edit:
Because React DND does a bit of magic behind the scenes, we have to use their API to get at the decorated component. They provide getDecoratedComponentInstance() so you can get at the underlying component. Once you use that, you can get the component.node as expected:
hover(props, monitor, component) {
const dragIndex = monitor.getItem().index;
const hoverIndex = props.index;
const rawComponent = component.getDecoratedComponentInstance();
console.log( rawComponent.node.getBoundingClientRect() );
...
Here it is in action:
https://jsfiddle.net/h4w4btz9/2/
Better Solution
A better solution is to just wrap your draggable component with a div, define a ref on that and pass it to the draggable component, i.e.
<div key={key} ref={node => { this.node = node; }}>
<MyComponent
node={this.node}
/>
</div>
and MyComponent is wrapped in DragSource. Now you can just use
hover(props, monitor, component) {
...
props.node && props.node.getBoundingClientRect();
...
}
(props.node && is just added to avoid to call getBoundingClientRect on an undefined object)
Alternative for findDOMNode
If you don't want to add a wrapping div, you could do the following.
The reply of #imjared and the suggested solution here don't work (at least in react-dnd#2.3.0 and react#15.3.1).
The only working alternative for findDOMNode(component).getBoundingClientRect(); which does not use findDOMNode is:
hover(props, monitor, component) {
...
component.decoratedComponentInstance._reactInternalInstance._renderedComponent._hostNode.getBoundingClientRect();
...
}
which is not very beautiful and dangerous because react could change this internal path in future versions!
Other (weaker) Alternative
Use monitor.getDifferenceFromInitialOffset(); which will not give you precise values, but is perhaps good enough in case you have a small dragSource. Then the returned value is pretty predictable with a small error margin depending on the size of your dragSource.
React-DnD's API is super flexible—we can (ab)use this.
For example, React-DnD lets us determine what connectors are passed to the underlying component. Which means we can wrap them, too. :)
For example, let's override the target connector to store the node on the monitor. We will use a Symbol so we do not leak this little hack to the outside world.
const NODE = Symbol('Node')
function targetCollector(connect, monitor) {
const connectDropTarget = connect.dropTarget()
return {
// Consumer does not have to know what we're doing ;)
connectDropTarget: node => {
monitor[NODE] = node
connectDropTarget(node)
}
}
}
Now in your hover method, you can use
const node = monitor[NODE]
const hoverBoundingRect = node.getBoundingClientRect()
This approach piggybacks on React-DnD's flow and shields the outside world by using a Symbol.
Whether you're using this approach or the class-based this.node = node ref approach, you're relying on the underlying React node. I prefer this one because the consumer does not have to remember to manually use a ref other than the ones already required by React-DnD, and the consumer does not have to be a class component either.

Categories

Resources