How to avoid circular update:modelValue event on VueJS3 component? - vuejs3

I have a VueJs component that allows the v-model="form.title" parameter:
<template>
<wysiwyg-editor :value="modelValue" #update="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
import WysiwygEditor from './WysiwygEditor'
export default {
emits: ['update:modelValue'],
components: { WysiwygEditor },
props: {
modelValue: {
type: String,
required: false,
default: null
}
},
watch: {
modelValue (to) {
this.editor.setValue(to)
}
}
}
</script>
Problem of that is that whenever the user writes in the input field, the data is $emit to the parent, which gets the new value and triggers down the watch on the current element, which updates the value back, causing writing issues.
I need to have the watch property because the parent can modify the value that needs to be reflected in this component.
I thought about using the mounted callback on the component, but the component stays on the page, so it isn't mounted at every change from the parent.
How can I do to avoid a circular call?

Related

Extjs beforerender to set async-obtained variable

My ExtJS application displays certain UI elements depending on a boolean variable.
This boolean variable, however, is the result of calling an async function. As a result, the boolean is set to a Promise that is fulfilled, rather than true or false proper. This affects whether the UI elements are actually displayed (a Promise is not exactly a boolean, after all).
The code looks like this:
Ext.define('userDefinedComponent', {
extend: 'Ext.Container',
requires: ['someHelperFile'],
initComponent: function () {
var me = this,
var enabled = someHelperFile.someAsyncFunc() // enabled is a boolean that is returned as fulfilled Promise instead
Ext.apply(me, {
// layout and padding
items: [
{
xtype: 'internallyDefinedForm',
fieldConfigs: {
// other fields
'someFormField': {
hidden: !enabled, // depends on enabled
}
}
},
{
xtype: 'internallyDefinedGrid',
columnConfigs: {
// other columns
'someColumn': {
hidden: !enabled, // deends on enabled
}
},
}
]
})
}
})
I want the field enabled to really be a boolean rather than a Promise. In other words, I want to wait for the someAsyncFunc to run the result, before setting hidden property of the internallyDefinedForm and internallyDefinedGrid.
What are my possibilities? I was thinking of using a beforerender, like below:
Ext.define('userDefinedComponent', {
extend: 'Ext.Container',
requires: ['someHelperFile'],
initComponent: function () {
var me = this;
Ext.apply(me, {
// layout and padding
items: [
{
xtype: 'internallyDefinedForm',
fieldConfigs: {
// other fields
'someFormField': {
hidden: !me.enabled, // depends on enabled
}
}
},
{
xtype: 'internallyDefinedGrid',
columnConfigs: {
// other columns
'someColumn': {
hidden: !me.enabled, // deends on enabled
}
},
listeners: {
beforerender: function() { // this is the beforerender
me.enabled = someHelperFile.someAsyncFunc();
console.log("beforerender triggered in grid");
}
},
}
]
})
}
})
And in fact, using the beforerender for the internallyDefinedGrid only, I can see the text "beforerender triggered in grid" triggered very early. However, the fact remains that the behavior that I observe does not correspond to what I expect: although the async someAsyncFunc should return true based on the API response it gets, such that me.enabled is true, the actual UI associated with the internallyDefinedGrid behaves as if me.enabled is false instead. I observe that the column on the UI is hidden, and this is only possible when me.enabled is false, such that the column someColumn does not appear on the grid. After all, the hidden field of someColumn is set to !enabled.
I am confident that the UI for the grid behaves not like what I expect it to, so there is a problem with the async behavior. But I'm really lost as to how to set the asynchronously obtained enabled or me.enabled field adequately.
Any help is appreciated.
I would suggest to use a View Model and binding, as explained here.
Basically you define what your UI is depending on, under the data tag in the View Model (you can set the initial value here):
Ext.define('MyApp.TestViewModel', {
extend: 'Ext.app.ViewModel',
data: {
something: false,
},
}
Then you bind the visibility to this value in the view:
bind: {
hidden: '{something}'
}
or
bind: {
hidden: '{!something}'
}
You fetch the async data, and once you have the result, set the value in the View Model (this can be either the view or the controller):
this.getViewModel().set('something', RESULT_OF_ASYNC)
With binding ExtJS takes care of refreshing the visibility of your component every time when the value in the View Model is changed. There are good examples at the link I provided. This is a very powerful and complex feature of ExtJS, worth learning.

Vue 3, Composition Api two way data binding on 'reactive' not working correctly

i am new in vue 3 and trying to make two data binding, but it's not working correctly. I did setup() and create a reactive data. Bind it to the children via v-model. Then $emit the updated data back to the parent. But parent's reactive data not applied 'till code re-run on save command.
parent script:
export default defineComponent({
setup(){
const activeCard = reactive({id:null, travelType:null})
return {activeCard}
},
})
parent template:
<pin-card v-for="(pinCard, idx) in categoryPinCards"
:key="idx"
v-model="activeCard"
:pinCard="pinCard"></pin-card>
child script
export default defineComponent({
props:{
modelValue:Object,
},
methods:{
makePinActive(id:number, travelType:null) {
this.$emit("update:modelValue", {id: id, travelType: travelType})
},
}
})
I followed the steps but, something is missing?
It seems reactive props can't be used by v-model in Vue 3 (while refs don't have this problem).
A workaround is to add a handler for the update:modelValue event that Object.assigns the new value:
<pin-card
v-for="(pinCard, idx) in categoryPinCards"
:key="idx"
:modelValue="activeCard" 👈
#update:modelValue="setActiveCard($event)" 👈
:pinCard="pinCard"
></pin-card>
export default {
setup() {
return {
//...
setActiveCard(eventData) {
Object.assign(activeCard, eventData)
}
}
}
}
demo

Detect change to modelValue in Vue 3

Is there a way to detect change to modelValue in a custom component? I want to push the change to a wysiwyg editor.
I tried watching modelValue but emitting update for modelValue triggered that watch, which created circular data flow.
Code:
export default {
props: ['modelValue'],
watch: {
modelValue (val) {
this.editor.editor.loadHTML(val)
}
},
mounted () {
this.editor.editor.loadHTML(val)
this.editor.addEventListener('trix-change',
(event) => this.$emit('update:modelValue', event.target.value))
}
}
<TextEditor v-model="someHtml"></TextEditor>
In VueJS v3, the event name for custom v-model handling changed to 'update:modelValue'.
You can listen to these events like this: v-on:update:modelValue="handler"
For a more complete example, lets assume you have a Toggle component with these properties/methods:
...
props: {
modelValue: Boolean,
},
data() {
return {
toggleState: false,
};
},
methods: {
toggle() {
this.toggleState = !this.toggleState;
this.$emit('update:modelValue', this.toggleState);
}
}
...
You can use that Toggle component:
<Toggle v-model="someProperty" v-on:update:modelValue="myMethodForTheEvent"/>
As a side note, you could also v-model on a computed property with a setter; allowing you to internalise your state changes without using the update:modelValue event. In this example, it assumes you v-model="customProperty" on your custom Toggle component.
computed: {
customProperty: {
get() {
return this.internalProperty;
},
set(v) {
this.internalProperty = v;
console.log("This runs when the custom component 'updates' the v-model value.");
},
}
},
I had the same problem and solved it using a slight tweak to the way you call the watch function:
setup(props) {
watch(() => props.modelValue, (newValue) => {
// do something
})
}
Hence, the important thing is to add () => props.modelValue instead of just putting props.modelValue as the first argument of the watch function.
try that:
watch: {
...
modelValue: function(val) {
console.log('!!! model value changed ', val);
},
...

How can I make vuefire show loading screen?

As title
Vuefire can auto get data from firebase database, but it needs some loading time.
So I want to display some css animation before data being fetched, is there any event can I $watch when it successed
The readyCallback approach in the other answer didn't work for me. I got an error document.onSnapshot is not a function.
Instead, I used the binding approach to set a flag when loading is complete.
<script>
// ...
export default {
data() {
return {
data: [],
loaded: false,
}
},
mounted() {
this.$bind('data', firebase.firestore().collection('someDocRef'))
.then(() => this.loaded = true);
},
}
</script>
Then my template can have conditionally-rendered loading screens:
<template>
<template v-if="!loaded">
<p>Loading...</p>
</template>
<template v-if="loaded">
<!-- Display data here -->
</template>
</template>
You can do this multiple ways.
Vuefire has readyCallback out of the box which is callback called when the data is fetched (ready).
Here it is:
var vm = new Vue({
el: '#demo',
data: function() {
return {
loaded: false
}
}
firebase: {
// simple syntax, bind as an array by default
anArray: db.ref('url/to/my/collection'),
// can also bind to a query
// anArray: db.ref('url/to/my/collection').limitToLast(25)
// full syntax
anObject: {
source: db.ref('url/to/my/object'),
// optionally bind as an object
asObject: true,
// optionally provide the cancelCallback
cancelCallback: function () {},
// this is called once the data has been retrieved from firebase
readyCallback: function () {
this.loaded = true // NOTE THIS LINE
}
}
}
})

Angular2: Two-way data binding on component dynamically inserted using DynamicComponentLoader

I am developing an Angular2 app, and I faced a problem:
I have a set of different objects that can be selected using UI. Each of this objects has a set of options (different for different objects) that could be edited using UI. Now, I am using DynamicComponentLoader to insert a specific component for currently selected object, so it can handle its options correctly.
The problem is that I don't know how to bind data of currently selected object to a dynamically inserted options component.
#Component({
selector: 'dynamic',
template: `<div>Options:</div>
<div>Property1: <input type="number" /></div>
<div>Property2: <input type="text" /></div>`
// template: `<div>Options:</div>
// <div>Property1: <input type="number" [(ng-model)]="currentSelection.property1" /></div>
// <div>Property2: <input type="text" [(ng-model)]="currentSelection.property1" /></div>`
})
class DynamicComponent {
}
#Component({
selector: 'my-app',
template: `
<div>
<h2>Selected: {{currentSelection.name}}!</h2>
<div #container></div>
</div>
`
})
class App {
currentSelection = {name: 'Selection1', property1: 10, property2: 'test'};
constructor(private loader: DynamicComponentLoader, private elementRef: ElementRef) {
loader.loadIntoLocation(DynamicComponent, elementRef, 'container');
}
}
Here is a plunker to help you understand my question:
With angular2 and Rxjs, "Observables" are almost always the answer.
If i understood your problem correctly, you need to make your DynamicComponent an "Observer" and your container "an Observable or even better a Subject (In case your container needs to subscribe to another observable to receive selections from)". Then, after loading your dynamic component, subscribe it to your container.
Whenever the selection changes on your container, you push the new selection to your subscribers. This way, you can load multiple dynamic components and all will receive your pushes.
The Container:
class App {
currentSelection = {};
selections = [
{name: 'Selection1', property1: 10, property2: 'test'},
{name: 'Selection2', property1: 20, property2: 'test2'}
];
subject:Subject<any> = new Subject();
constructor(private loader: DynamicComponentLoader, private elementRef: ElementRef) {
}
ngOnInit(){
this.loader.loadIntoLocation(DynamicComponent, this.elementRef, 'container', this.injector)
.then(compRef =>this.subject.subscribe(compRef.instance));
// subscribe after loading the dynamicComponent
}
// set the new selection and push it to subscribers
changeSelection(newSelection){
this.currentSelection = newSelection;
this.subject.next(this.currentSelection);
}
}
The Observer:
class DynamicComponent implements Observer{
public currentSelection = {};
next(newSelection){
this.currentSelection = newSelection;
}
}
Here is your plunker working after my edits, "provided I changed the imports to the newest angular beta.6"
I know this is a quite old question. But hopefully someone will benefit from this answer.
Here is what you can do, move your code from constructor to ngOnInit and use promises for assigning dynamic value.
ngOnInit(){
this.dynamicComponentLoader.loadIntoLocation(DynamicComponent, this.elementRef,'container').then((component)=>{
component.instance.currentSelection = currentSelection;
});
}

Resources