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);
},
...
Related
I have an object in my pinia store like
import { defineStore } from "pinia";
export const useSearchStore = defineStore("store", {
state: () => {
return {
myobj: {
foo: 0,
bar: 2000,
too: 1000,
},
};
},
getters: {
changed() {
// doesn't work
return Object.entries(this.myobj).filter(([key, value]) => value != initialvalue
);
},
},
});
How do I get the initial value to test if the object changed. Or how can I return a filtered object with only those entries different from initial state?
My current workaround:
in a created hook I make a hard copy of the store object I then can compare to. I guess there is a more elegant way...
I had done this (although I do not know if there a better way to avoid cloning without duplicating your initial state).
Define your initial state outside and assign it to a variable as follows;
const initialState = {
foo: 0,
bar: 2000,
too: 1000
}
Then you can use cloning to retain the original state;
export const useSearchStore = defineStore("store", {
state: () => {
return {
myobj: structuredClone(initialState),
};
},
getters: {
changed: (state) => deepEquals(initialState, state.myobj);
},
});
where deepEquals is a method which deep compares the two objects (which you would have to implement). I would use lodash (npm i lodash and npm i #types/lodash --save-dev if you're using TypeScript) for this.
Full code (with lodash);
import { defineStore } from "pinia";
import { cloneDeep, isEqual } from "lodash";
const initialState = {
foo: 0,
bar: 2000,
too: 1000
}
export const useSearchStore = defineStore("store", {
state: () => ({
myobj: cloneDeep(initialState)
}),
getters: {
changed(state) {
return isEqual(initialState, state.myobj);
},
},
});
If you also want the differences between the two you can use the following function (the _ is lodash - import _ from "lodash");
function difference(object, base) {
function changes(object, base) {
return _.transform(object, function (result: object, value, key) {
if (!_.isEqual(value, base[key])) {
result[key] =
_.isObject(value) && _.isObject(base[key])
? changes(value, base[key])
: value;
}
});
}
return changes(object, base);
}
courtesy of https://gist.github.com/Yimiprod/7ee176597fef230d1451
EDIT:
The other way you would do this is to use a watcher to subscribe to changes. The disadvantage to this is that you either have to be OK with your state marked as "changed" if you change back the data to the initial state. Otherwise, you would have to implement a system (perhaps using a stack data structure) to maintain a list of changes so that if two changes which cancel each other out occur then you would remark the state as "unchanged". You would have to keep another variable (boolean) in the state which holds whether the state has been changed/unchanged - but this would be more complicated to implement and (depending on your use case) not worth it.
Kind of a niche question, but I know what the issue is so hopefully someone here can help me out. This is an Observable/RXFire issue, not an xstate issue.
I have this machine that invokes an observable:
export const tribeMachine = Machine(
{
id: "council",
initial: "init",
context: {},
states: {
init: {
invoke: {
id: "gettribes",
src: () =>
collectionData(database.collection("tribes")).pipe(
concatAll(),
map(x => ({ type: "STORE", x }))
),
onDone: "loaded"
},
on: {
STORE: {
actions: "storetribes"
},
CANCEL: "loaded"
}
},
loaded: {
entry: () => console.log("loaded")
},
error: {
entry: () => console.log("error")
}
}
},
{
actions: {
storetribes: (context, event) => console.log("hello")
}
}
);
The way it's supposed to work is that the machine invokes the observable on load, and then once the obs is done emitting its values and calls complete(), invoke.onDone is called and the machine transitions to the 'loaded' state.
When I use a normal observable that i created with a complete() call, or when i add take(#) to the end of my .pipe(), the transition works.
But for some reason the observable that comes from collectionData() from RXFire doesn't send out a 'complete' signal... and the machine just sits there.
I've tried adding a empty() to the end and concat()-ing the observables to add a complete signal to the end of the pipe... but then I found out that empty() is deprecated and it didn't seem to work anyway.
Been banging my head against the wall for awhile. any help is appreciated.
Edit:
Solution:
I misunderstood the purpose of collectionData(). It is a listener, so it's not supposed to complete. I was putting a square peg in round hole. The solution is to refactor the xstate machine so I don't need to call onDone at all.
Thank you for the answers nonetheless.
EDIT2: GOT IT TO WORK.
take(1) can be called BEFORE concatAll(). I thought if you called it first it would end the stream, but it doesn't. The rest of the operators in the pipe still apply. So i take(1) to get the single array, use concatAll() to flatten the array into a stream of individual objects, then map that data to a new object which triggers the STORE action. the store action then sets the data to the context of the machine.
export const tribeMachine = Machine({
id: 'council',
initial: 'init',
context: {
tribes: {},
markers: []
},
states: {
init: {
invoke: {
id: 'gettribes',
src: () => collectionData(database.collection('tribes')).pipe(
take(1),
concatAll(),
map(value => ({ type: 'TRIBESTORE', value })),
),
onDone: 'loaded'
},
on: {
TRIBESTORE: {
actions: ['storetribes', 'logtribes']
},
CANCEL: 'loaded'
}
},
loaded: {
},
error: {
}
}
},
{
actions: {
storetribes: assign((context, event) => {
return {
tribes: {
...context.tribes,
[event.value.id]: event.value
},
markers: [
...context.markers,
{
lat: event.value.lat,
lng: event.value.lng,
title: event.value.tribeName
}
]
}
})
}
}
)
Thanks for everyone's help!
Observables can return multiple values over time, so it is up to collectionData() to decide when to finish (i.e. causing complete() to be called).
However, if you only want to take 1 value from the observable, you can try:
collectionData(database.collection("tribes")).pipe(
take(1),
concatAll(),
map(x => ({ type: "STORE", x }))
),
This will cause the observable to complete once you take 1 value from collectionData().
Note: This may not be the best solution as it depends on how the observable streams you are using works. I am just highlighting that you can use take(1) to just take 1 value and complete the source observable.
I have a custom select box.
<select-box :options="['Male', 'Female', ]"
title="Gender"
v-bind:value="selected"
v-model="person.gender"
>
</select-box>
The .vue code
<script>
export default {
props:['title', 'options'],
data () {
return {
selected: this.title,
dropdownVisible: false,
}
},
methods: {
toggleOptions() {
this.dropdownVisible = !this.dropdownVisible
},
selectValue(option) {
this.selected = option;
this.toggleOptions();
}
}
}
How can I bind the selected value directly to the model (person.gender)?
I assume that above .vue code belongs to select-box component.
Because I saw you use v-model, to bind value directly to v-model, you need to $emit inside children component.
You can change your selectValue function
selectValue(option) {
this.selected = option;
this.$emit('input', option);
this.toggleOptions();
}
I have made this simple property (Polymer 2.x):
static get properties() {
return {
bpm: {
type: Number,
value: () => {
return 0
},
observer: "_bpm"
}
}
}
I tried to update it using this.bpm = 60; in a function called when clicking a button. If I output the value using console.log(this.bpm); it displays the correct value, but my heading <h2 id="bpm">[[bpm]]</h2> is not updated and the observer is not called.
When bpm is set using something like <paper-slider value="{{bpm}}"></paper-slider> it works.
What am I doing wrong? Thank you for your help!
It will be easier for the community to know that this question was answered into the comments of the requests.
Initial problem : Binding value not updated because bpm property was set from a function outside of the element.
Correction : Here a working JSFiddle (to use in chrome) used to demonstrate how to use the binding.
I also faced similar issue due to setting the property from a different function. Putting it here for reference.
My code:
Polymer({
is: 'test-test',
properties: {
min: {
type: Number,
value: -1,
observer: '_minChangedd'
}
},
_minChangedd: function (val) {
console.log(val);
},
ready: function () {
setInterval(function () {
this.min = this.min + 1;
}, 500);
},
});
Problem:
The setInterval function had its own this and so the expression this.min actually refers to min of setInterval.
Using arrow functions resolved the issue, by replacing the call with setInterval(() => {...});
As far as my understanding goes, it's an anti-pattern to dispatch actions from within a store update handler. Correct?
How can I handle the following workflow then?
I have some company switcher on my page header
Clicking on a company dispatches some SELECTEDCOMPANY_UPDATE action
The active view reacts on the according change in the state store by forcing a data reload. E.g. by calling companyDataService.fetchOrders(companyName).
I'd like to show some loading animation during the data is being fetched and therefore have an dedicated action like FETCHINGDATA_UPDATE which updates the fetchingData section in my app state store to which all interested views can react by showing/hiding the load mask
Where do I actually dispatch the FETCHINGDATA_UPDATE action? If I directly do this from within companyDataService.fetchOrders(companyName) it would be called from within a store update handler (see OrdersView.onStoreUpdate in exemplary code below)...
Edit
To clarify my last sentence I'm adding some exemplary code which shows how my implementation would have looked like:
ActionCreator.js
// ...
export function setSelectedCompany(company) {
return { type: SELECTEDCOMPANY_UPDATE, company: company };
}
export function setFetchingData(isFetching) {
return { type: FETCHINGDATA_UPDATE, isFetching: isFetching };
}
// ...
CompanyDataService.js
// ...
export fetchOrders(companyName) {
this.stateStore.dispatch(actionCreator.setFetchingData(true));
fetchData(companyName)
.then((data) => {
this.stateStore.dispatch(actionCreator.setFetchingData(false));
// Apply the data...
})
.catch((err) => {
this.stateStore.dispatch(actionCreator.setFetchingData(false));
this.stateStore.dispatch(actionCreator.setFetchError(err));
})
}
// ...
CompanySwitcher.js
// ...
onCompanyClicked(company) {
this.stateStore.dispatch(actionCreator.setSelectedCompany(company));
}
// ...
OrdersView.js
// ...
constructor() {
this._curCompany = '';
this.stateStore.subscribe(this.onStoreUpdate);
}
// ...
onStoreUpdate() {
const stateCompany = this.stateStore.getState().company;
if (this._curCompany !== stateCompany) {
// We're inside a store update handler and `fetchOrders` dispatches another state change which is considered bad...
companyDataService.fetchOrders(stateCompany);
this._curCompany = stateComapny;
}
}
// ...
I agree with Davin, in the action creator is the place to do this, something like:
export function fetchOrders (company) {
return (dispatch) => {
dispatch ({ type: FETCHINGDATA_UPDATE });
return fetchOrderFunction ().then(
(result) => dispatch ({ type: FETCHING_COMPLETED, result }),
(error) => dispatch ({ type: FETCHING_FAILED, error })
);
};
}
Then in the reducer FETCHINGDATA_UPDATE can set your loading indicator to true and you can set it back to false I both SUCCESS and FAILED