Vuejs 2.3 - Sync and Element Ui Dialog - javascript

I'm using Element UI and things have changed since the release of Vue.js 2.3
I have a dialog that should be displayed only if the following condition is met private.userCanManageUsers && private.pendingUsers.length > 0 && private.pendingDialogVisible
I'm trying to use the new attribute visible.sync documentation here
It is working if the condition contains only one condition but does not work with several.
Working
<el-dialog
:visible.sync="private.pendingDialogVisible"
</el-dialog>
Not working
<el-dialog
:visible.sync="private.userCanManageUsers && private.pendingUsers.length > 0 && private.pendingDialogVisible"
</el-dialog>
What is the solution to use the el-dialog with visible.sync with
several condition?
If this is impossible what should I do to make it work ?

The issue is caused by a misunderstanding of what sync is actually doing.
In Vue 2.3 (unlike in Vue 1x), sync is nothing more than an event registration to facilitate two-way binding. Per the documentation:
In 2.3 we re-introduced the .sync modifier for props, but this time it
is just syntax sugar that automatically expands into an additional
v-on listener:
The following
<comp :foo.sync="bar"></comp>
is expanded into:
<comp :foo="bar" #update:foo="val => bar = val"></comp>
What does this mean in layman's terms? Since it is facilitating two-way binding to update the value being sync'd upon, you cannot use multiple properties (as a boolean expression), nor can you use the return value of a method since you must both read from and write to the same value.
In short, no, you cannot accomplish using sync in the way you are currently utilizing it and I personally disagree with the implementation that the library has chosen since it isn't particularly clear and forces complicated workarounds.
That said, you can use a single property for binding the visibility of :visible.sync and you can trigger that based on your state in the following example:
Template:
<div id="app">
<el-dialog title="Shipping address" :visible.sync="showDialog"
:before-close="beforeCloseHandler"
#close="cond1 = true; cond2 = false;">
</el-dialog>
<button #click="cond1 = true; cond2 = false; showDialog = true;">Open Dialog</button>
</div>
Javascript:
var Main = {
data() {
return {
showDialog: true,
cond1: true,
cond2: true,
};
},
methods: {
beforeCloseHandler: function (done) {
if (this.cond1 && this.cond2) {
console.log('hit close');
done();
} else {
console.log('rejected close');
}
}
}
};
var Ctor = Vue.extend(Main)
new Ctor().$mount('#app')
We can bind the display to a single property and we can control dismissing with the :before-close handler and of course we can control our show conditions via a click event on a button. It isn't perfect, but it is a potential workaround.

Related

How to manually trigger react state update with browser javascript [duplicate]

We use Backbone + ReactJS bundle to build a client-side app.
Heavily relying on notorious valueLink we propagate values directly to the model via own wrapper that supports ReactJS interface for two way binding.
Now we faced the problem:
We have jquery.mask.js plugin which formats input value programmatically thus it doesn't fire React events. All this leads to situation when model receives unformatted values from user input and misses formatted ones from plugin.
It seems that React has plenty of event handling strategies depending on browser. Is there any common way to trigger change event for particular DOM element so that React will hear it?
For React 16 and React >=15.6
Setter .value= is not working as we wanted because React library overrides input value setter but we can call the function directly on the input as context.
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, 'react 16 value');
var ev2 = new Event('input', { bubbles: true});
input.dispatchEvent(ev2);
For textarea element you should use prototype of HTMLTextAreaElement class.
New codepen example.
All credits to this contributor and his solution
Outdated answer only for React <=15.5
With react-dom ^15.6.0 you can use simulated flag on the event object for the event to pass through
var ev = new Event('input', { bubbles: true});
ev.simulated = true;
element.value = 'Something new';
element.dispatchEvent(ev);
I made a codepen with an example
To understand why new flag is needed I found this comment very helpful:
The input logic in React now dedupe's change events so they don't fire
more than once per value. It listens for both browser onChange/onInput
events as well as sets on the DOM node value prop (when you update the
value via javascript). This has the side effect of meaning that if you
update the input's value manually input.value = 'foo' then dispatch a
ChangeEvent with { target: input } React will register both the set
and the event, see it's value is still `'foo', consider it a duplicate
event and swallow it.
This works fine in normal cases because a "real" browser initiated
event doesn't trigger sets on the element.value. You can bail out of
this logic secretly by tagging the event you trigger with a simulated
flag and react will always fire the event.
https://github.com/jquense/react/blob/9a93af4411a8e880bbc05392ccf2b195c97502d1/src/renderers/dom/client/eventPlugins/ChangeEventPlugin.js#L128
At least on text inputs, it appears that onChange is listening for input events:
var event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
Expanding on the answer from Grin/Dan Abramov, this works across multiple input types. Tested in React >= 15.5
const inputTypes = [
window.HTMLInputElement,
window.HTMLSelectElement,
window.HTMLTextAreaElement,
];
export const triggerInputChange = (node, value = '') => {
// only process the change on elements we know have a value setter in their constructor
if ( inputTypes.indexOf(node.__proto__.constructor) >-1 ) {
const setValue = Object.getOwnPropertyDescriptor(node.__proto__, 'value').set;
const event = new Event('input', { bubbles: true });
setValue.call(node, value);
node.dispatchEvent(event);
}
};
I know this answer comes a little late but I recently faced a similar problem. I wanted to trigger an event on a nested component. I had a list with radio and check box type widgets (they were divs that behaved like checkboxes and/or radio buttons) and in some other place in the application, if someone closed a toolbox, I needed to uncheck one.
I found a pretty simple solution, not sure if this is best practice but it works.
var event = new MouseEvent('click', {
'view': window,
'bubbles': true,
'cancelable': false
});
var node = document.getElementById('nodeMyComponentsEventIsConnectedTo');
node.dispatchEvent(event);
This triggered the click event on the domNode and my handler attached via react was indeed called so it behaves like I would expect if someone clicked on the element. I have not tested onChange but it should work, and not sure how this will fair in really old versions of IE but I believe the MouseEvent is supported in at least IE9 and up.
I eventually moved away from this for my particular use case because my component was very small (only a part of my application used react since i'm still learning it) and I could achieve the same thing another way without getting references to dom nodes.
UPDATE:
As others have stated in the comments, it is better to use this.refs.refname to get a reference to a dom node. In this case, refname is the ref you attached to your component via <MyComponent ref='refname' />.
You can simulate events using ReactTestUtils but that's designed for unit testing.
I'd recommend not using valueLink for this case and simply listening to change events fired by the plugin and updating the input's state in response. The two-way binding utils more as a demo than anything else; they're included in addons only to emphasize the fact that pure two-way binding isn't appropriate for most applications and that you usually need more application logic to describe the interactions in your app.
I stumbled upon the same issue today. While there is default support for the 'click', 'focus', 'blur' events out of the box in JavaScript, other useful events such as 'change', 'input' are not implemented (yet).
I came up with this generic solution and refactored the code based on the accepted answers.
export const triggerNativeEventFor = (elm, { event, ...valueObj }) => {
if (!(elm instanceof Element)) {
throw new Error(`Expected an Element but received ${elm} instead!`);
}
const [prop, value] = Object.entries(valueObj)[0] ?? [];
const desc = Object.getOwnPropertyDescriptor(elm.__proto__, prop);
desc?.set?.call(elm, value);
elm.dispatchEvent(new Event(event, { bubbles: true }));
};
How does it work?
triggerNativeEventFor(inputRef.current, { event: 'input', value: '' });
Any 2nd property you pass after the 'event' key-value pair, it will be taken into account and the rest will be ignored/discarded.
This is purposedfully written like this in order not to clutter arguments definition of the helper function.
The reason as to why not default to get descriptor for 'value' only is that for instance, if you have a native checkbox <input type="checkbox" />, than it doesn't have a value rather a 'checked' prop/attribute. Then you can pass your desired check state as follows:
triggerNativeEventFor(checkBoxRef.current, { event: 'input', checked: false });
I found this on React's Github issues: Works like a charm (v15.6.2)
Here is how I implemented to a Text input:
changeInputValue = newValue => {
const e = new Event('input', { bubbles: true })
const input = document.querySelector('input[name=' + this.props.name + ']')
console.log('input', input)
this.setNativeValue(input, newValue)
input.dispatchEvent(e)
}
setNativeValue (element, value) {
const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set
const prototype = Object.getPrototypeOf(element)
const prototypeValueSetter = Object.getOwnPropertyDescriptor(
prototype,
'value'
).set
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value)
} else {
valueSetter.call(element, value)
}
}
For HTMLSelectElement, i.e. <select>
var element = document.getElementById("element-id");
var trigger = Object.getOwnPropertyDescriptor(
window.HTMLSelectElement.prototype,
"value"
).set;
trigger.call(element, 4); // 4 is the select option's value we want to set
var event = new Event("change", { bubbles: true });
element.dispatchEvent(event);
Triggering change events on arbitrary elements creates dependencies between components which are hard to reason about. It's better to stick with React's one-way data flow.
There is no simple snippet to trigger React's change event. The logic is implemented in ChangeEventPlugin.js and there are different code branches for different input types and browsers. Moreover, the implementation details vary across versions of React.
I have built react-trigger-change that does the thing, but it is intended to be used for testing, not as a production dependency:
let node;
ReactDOM.render(
<input
onChange={() => console.log('changed')}
ref={(input) => { node = input; }}
/>,
mountNode
);
reactTriggerChange(node); // 'changed' is logged
CodePen
well since we use functions to handle an onchange event, we can do it like this:
class Form extends Component {
constructor(props) {
super(props);
this.handlePasswordChange = this.handlePasswordChange.bind(this);
this.state = { password: '' }
}
aForceChange() {
// something happened and a passwordChange
// needs to be triggered!!
// simple, just call the onChange handler
this.handlePasswordChange('my password');
}
handlePasswordChange(value) {
// do something
}
render() {
return (
<input type="text" value={this.state.password} onChange={changeEvent => this.handlePasswordChange(changeEvent.target.value)} />
);
}
}
The Event type input did not work for me on <select> but changing it to change works
useEffect(() => {
var event = new Event('change', { bubbles: true });
selectRef.current.dispatchEvent(event); // ref to the select control
}, [props.items]);
This ugly solution is what worked for me:
let ev = new CustomEvent('change', { bubbles: true });
Object.defineProperty(ev, 'target', {writable: false, value: inpt });
Object.defineProperty(ev, 'currentTarget', {writable: false, value: inpt });
const rHandle = Object.keys(inpt).find(k => k.startsWith("__reactEventHandlers"))
inpt[rHandle].onChange(ev);
A working solution can depend a bit on the implementation of the onChange function you're trying to trigger. Something that worked for me was to reach into the react props attached to the DOM element and call the function directly.
I created a helper function to grab the react props since they're suffixed with a hash like .__reactProps$fdb7odfwyz
It's probably not the most robust but it's good to know it's an option.
function getReactProps(el) {
const keys = Object.keys(el);
const propKey = keys.find(key => key.includes('reactProps'));
return el[propKey];
}
const el = document.querySelector('XX');
getReactProps(el).onChange({ target: { value: id } });
Since the onChange function was only using target.value I could pass a simple object to onChange to trigger my change.
This method can also help with stubborn react owned DOM elements that are listing for onMouseDown and do not respond to .click() like you'd expect.
getReactProps(el).onMouseDown(new Event('click'));
If you are using Backbone and React, I'd recommend one of the following,
Backbone.React.Component
react.backbone
They both help integrate Backbone models and collections with React views. You can use Backbone events just like you do with Backbone views. I've dabbled in both and didn't see much of a difference except one is a mixin and the other changes React.createClass to React.createBackboneClass.

Using React components inside KnockoutJS

We have a big web application mostly built using KnockoutJS. I'm looking at if there is a possibility to migrate to React, but in a way that require rewriting the entire application. My idea was to use a bottom-up approach: start by replacing the basic building blocks one by one.
I was inspired by some prior work to call ReactDOM.render inside a KnockoutJS binding handler:
ko.bindingHandlers.react = {
init() {
return {controlsDescendantBindings: true};
},
update(element, valueAccessor) {
const {component, props} = valueAccessor();
ReactDOM.render(React.createElement(component, props), element);
}
};
Knockout has its own dependency tracking system, so it will call the update method whenever some data changes.
It works perfectly, and the component is re-rendered to reflect any changes to data. However, it breaks down when data is updated inside a React event handler. E.g., this component does not work as expected:
const Input = function ({value}) {
return <input type="text"
value={value()}
onChange={e => value(e.target.value)}/>;
}
Note: value in this case is a ko.observable, and calling it like in the event handler will cause the bindingHandler's update method to be called, which in turn calls ReactDOM.render. However, this render only works once, after that the component stops updating.
The issue is demonstrated in this CodePen. Click the box and try to type something. One update goes through, after than, the component's function stops being called.
Edit: I believe the issue is that the second call to ReactDOM.render (when the value is updated by the user in the onChange handler) is not synchronous. This means that Knockout's dependency detection cannot work and any subsequent calls no longer call the update method of the binding handler.
Can this be circumvented somehow?
As I guessed, the issue seems to be that ReactDOM.render is asynchronous in some cases - in this case when called from an event handler in React.
This means that if you dereference any observables that you depend on in the update method itself, Knockout's dependency tracking mechanism works as expected. This is why the modification Gawel1908 proposed makes it work - not because the value is "reset", but because props.value is dereferenced.
I decided to instead use a convention: always unwrap any such observables in the valueAccessor itself:
<div data-bind="react: { component: Input, props: { value: val(), setValue: val }}">
</div>
And don't unwrap it in the component:
const Input = function ({value, setValue}) {
return <input
type="text"
value={value}
onChange={e => setValue(e.target.value)}/>;
}
Updated, working codepen.
For me worked:
update(element, valueAccessor) {
const {component, props} = valueAccessor();
props.value(props.value());
ReactDOM.render(React.createElement(component, props), element);
}
if you refresh your observable it will work but unfortunetaly i don't know how.

My angular 7 web component #Input<boolean> doesn't work when I bind a false value

I have setup an application which exports web components from angular 7 using angular custom-elements package.
Everything works fine. I am able to bind anything, arrays, objects, strings from javascript using the element instance:
const table = document.createElement('my-table-element');
table.someInputProp = 'Works Great';
table.otherInput = ['my', 'working', 'array'];
The problem comes when I try to bind to a literal false value:
table.myBooleanInput = false
This doesnt change anything on the component #Input myBooleanInput = true
The value is still true for ever. No matter how many times it changes to false.
I'am able to bind it as a truthy value which it renders just fine. The problem is only when using literal false.
Here is a working reproduction of this issue:
https://stackblitz.com/edit/angular-elements-official-example-q4ueqr
Thanks in advance.
PS: If I use the web components from within another angular app, the binding works fine.
I'm actually having this same issue. Was debugging it and there seems to be some code the #angular/elements (I'm using version 7.2.15) package which skips over setting properties on initialize when they evaluate to false.
/** Set any stored initial inputs on the component's properties. */
ComponentNgElementStrategy.prototype.initializeInputs = function () {
var _this = this;
this.componentFactory.inputs.forEach(function (_a) {
var propName = _a.propName;
var initialValue = _this.initialInputValues.get(propName);
if (initialValue) {
_this.setInputValue(propName, initialValue);
}
else {
// Keep track of inputs that were not initialized in case we need to know this for
// calling ngOnChanges with SimpleChanges
_this.uninitializedInputs.add(propName);
}
});
this.initialInputValues.clear();
};
As a workaround you could convert the flag to a string, wrap it in an object, or look at the element attributes from within the component.

How to get React setState to re-render for Foundation 6 Reveal (Modal)?

My issue has been touched in a few questions around the web but I don't think it's been holistically asked.
I am using Foundation 6 with React. Everything works by using
import $ from 'jquery';
window.jQuery = $;
let foundation = require('path/to/foundation.js');
then in componentDidUpdate(), I call $(document).foundation(). Also, the CSS is being called somewhere.
My problem is once I get the modal to open, I can't populate it with data using setState(). I think I understand that the DOM changes when the modal opens, thus causing issues but I was wondering if anyone has had success with the Reveal plugin? My code is like this:
getData() {
Facebook.get('/me/taggable_friends', function(error, response) {
$('#modal').foundation('open');
//setTimeout is just for testing sanity
let _this = this;
setTimeout(function() {
_this.setState({ friends: response.data });
}, 3000);
})
}
Again, everything works. I'm getting data back from Facebook, the state is updating, the modal is opening, I'm just not able to populate the modal and I have this as my markup:
<div id="modal" className="modal-classes-from-foundation" data-reveal>
{
this.state.friends.length > 0 &&
this.state.friends.map((friend, i) => {
return(
<div>{ friend.name }</div>
)
})
}
</div>
Also to note, this.state.friends is being set in the constructor as an empty array.
All the code is valid on my server (no errors), but I wrote this from memory so I didn't remember small details like class/path names
Things I tried
Using componentWillReceiveProps to force update
Setting state before calling modal open
I dont think your answer is so smart. AFAIK the current state of the art is to set a nodeReference using a setNode method and then have
ref={this.setNode}
in your element, so then you can call whatever you are calling with jQuery, passing it via argument the node so you can play with it, and use the React lifecycle hooks to sync your react world with your jQuery playground
componentWillMount() {
window.addEventListener('resize', this.handleWindowResize, false);
}
componentDidMount() {
//initialize your jQuery dom manipulation
}
shouldComponentUpdate(nextProps) {
if (!this.props.store.equals(nextProps)) {
//to update stuff
}
return false; //> dont update
}
componentWillUnmount() {
//
}
setNode(ref) {
this.nodeReference = ref;
}
After banging my head against a virtual desk for a day, I understand why it's tough getting Reveal to work with React, especially compared to the other Foundation elements.
Reveal is an overlay that lives inside of <body></body> but not inside <div id="app"></div>, or whatever you name the root div that your React app renders to. Since the overlay is outside of the "app", React has no control over it and sending states/props to it won't do anything.
What I ended up doing is a bit clever and could be looked down upon, but seemed very necessary. I took inspiration from BlackMutt in 2015 where he basically created a function to use jQuery for appending the modal's code and initializing it. Unfortunately, this means every other thing you do with the modal will need to use jQuery as well but the good news is that it's separated from the rest of the app. Here's a sample of what it looks like:
createListModal(items) {
let content = items.map((item, i) => {
return $('<div class="list-item"><div class="item-name">'+ item.name +'</div></div>');
});
let close = $('<button class="close-button" data-close aria-label="Close modal" type="button"><span aria-hidden="true">×</span></button>');
let modal = $('<div class="reveal" id="list-popup" data-reveal>').append(content).append(pagination).append(close);
$(modal).foundation();
$('#list-popup').foundation('open');
}
So all I did was call that method when I got my data from Facebook. It's pretty simple but if you've been in React for a while, you have to switch your brain back into thinking in jQuery again.

Error when using property that relies on ViewChildren

I have created a custom component that contains a form <address></address>. And I have a parent component that has an array of these:
#ViewChildren(AddressComponent) addressComponents: QueryList<AddressComponent>;
So the parent can contain a collection of these elements and the user can add and remove them based on the number of addresses they will be entering.
The parent also has a button to proceed after the user has entered all desired addresses. However, the <address> component must be filled out correctly so I have a public getter on the <address> component:
get valid(): boolen {
return this._form.valid;
}
Back to the button on the parent. It needs to be disabled if any of the <address> components are invalid. So I wrote the following:
get allValid() {
return this.addressComponents && this.addressComponents.toArray().every(component => component.valid);
}
And in the parent template:
<button [disabled]="!allValid" (click)="nextPage()">Proceed</button>
But angular doesn't like this because addressComponents are not defined in the parent until ngAfterViewInit lifecycle event. And since it immediately runs ngOnViewInit() I get two different values for the expression check which causes the error. (At least that's what I think is going on).
How do I use a property in my template that depends on ngAfterViewInit? Or what is the best way to inform my parent that all of its children are valid?
The Error Message:
Expression has changed after it was checked. Previous value: 'false'.
Current value: 'true'
Update:
So I console.loged the return value of allValid and noticed the first time it was undefined. This was to be expected as this.addressComponents are undefined until ngAfterInit. The next log it was true and this was surprising as I didn't have any <address> components on the page (yet). I am using mock data (all valid, though) in ngOnInit of the parent component to create a component. I did learn that ([].every... returns true on an empty array). So the third call to the console.log was returning false. Again, I am a little surprised because all my data is valid. On the 4th log it was returning true which is what I expected. So I'm assuming this final value being returned is what Angular disliked.
Anyway, I was able to sort of solve this. I don't know if I'm actually fixing the problem or just suppressing the error. I do not like this solution so I am going to keep the question open for a better solution.
get allValid() {
return this.addressComponents && this.addressComponents.length > 0 && this.addressComponents().toArray().every(component => component.valid);
}
So what I think is happening:
The first wave of change detection gets you a false for your function, then your parent component finds out this information after the view is instantiated (then returns true). In "dev" mode, Angular runs change detection twice to ensure that changes don't happen AFTER change detection (as change detection should detect all of the changes, of course!)
According to the answer found here:
Angular2 - Expression has changed after it was checked - Binding to div width with resize events
using AfterViewInit can cause these issues, as it may run after the change detection has completed.
Wrapping your assignment in a timeout will fix this, as it will wait a tick before setting the value.
ngAfterViewInit(){
setTimeout(_ => this.allValid = this.addressComponents && this.addressComponents.toArray().every(component => component.valid));
}
Due to these reasons, I would not use a getter on a template variable like that, as the view initializing may change the value after change detection has finished.
If I understand, you'll probably need to come up with a way in the parent to track how many instances of the child there are, and the child will need an EventEmitter that informs the parent when it's valid or becomes invalid.
So in the parent you could use an array to track how many address instances there are..
Parent Component
addressForms: Array<any> = [{ valid: false }];
addAddressForm() {
this.addressForms.push({ valid: false ));
}
checkValid() {
// somehow loop through the addressForms, make sure all valid
let allValid: boolean = false;
for (var i = this.addressForms.length - 1; i >= 0; i--) {
if (this.addressForms[i].value && allValid === false)
allValid = true;
}
return allValid;
}
Parent Template
<div *ngFor="let form of addressForms; let i = index">
<address (valid)="form.valid = true" (invalid)="form.valid = false"></address>
</div>
<button [disabled]="checkValid()">Next</button>
Address Component
#Output() valid: EventEmitter<any> = new EventEmitter();
#Output() invalid: EventEmitter<any> = new EventEmitter();
isValid: boolean = false;
check() {
// call this check on field blurs and stuff
if ("it's valid now" && !this.isValid) {
this.isValid = true;
this.valid.emit(null);
}
if ("it's not valid anymore" && this.isValid) {
this.isValid = false;
this.invalid.emit(null);
}
}
That's the basic idea anyway, with some holes that are obvious enough to fill in. Hope that has some relevancy with what you're doing and I understood the question to begin with. Good luck!
I faced the same issue when have been using that realy handy pattern :( Only short way I found atm to solve it is the next kind of a hack:
#ViewChild(DetailsFormComponent) detailsForm: DetailsFormComponent;
isInitialized = false;
get isContinueBtnEnabled(): boolean {
return this.isInitialized && this.detailsForm?.isValid();
}
and
ngAfterViewInit() {
setTimeout(() => { // required
this.isInitialized = true;
});
}

Categories

Resources