I am creating undo/redo functionality in VueJS. I watch the settings and add a new element to an array of changes when the settings change. I also have a method for undo when the undo button is clicked.
However, when the button is clicked and the last setting is reverted, the settings are changed and the watch is fired again.
How can I prevent a new element being added to the array of changes if the settings changed but it was because the Undo button was clicked?
(function () {
var Admin = {};
Admin.init = function () {
};
var appData = {
settings: {
has_border: true,
leave_reviews: true,
has_questions: true
},
mutations: [],
mutationIndex: null,
undoDisabled: true,
redoDisabled: true
};
var app = new Vue({
el: '#app',
data: appData,
methods: {
undo: function() {
if (this.mutations[this.mutationIndex - 1]) {
let settings = JSON.parse(this.mutations[this.mutationIndex - 1]);
this.settings = settings;
this.mutationIndex = this.mutations.length - 1;
console.log (settings);
}
},
redo: function() {
}
},
computed: {
border_class: {
get: function () {
return this.settings.has_border ? ' rp-pwb' : ''
}
},
undo_class: {
get: function () {
return this.undoDisabled ? ' disabled' : ''
}
},
redo_class: {
get: function () {
return this.redoDisabled ? ' disabled' : ''
}
}
},
watch: {
undoDisabled: function () {
return this.mutations.length;
},
redoDisabled: function () {
return this.mutations.length;
},
settings: {
handler: function () {
let mutation = JSON.stringify(this.settings),
prevMutation = JSON.stringify(this.mutations[this.mutations.length-1]);
if (mutation !== prevMutation) {
this.mutations.push(mutation);
this.mutationIndex = this.mutations.length - 1;
this.undoDisabled = false;
}
},
deep: true
}
}
});
Admin.init();
})();
Since you make the changes with a button click, you can create a method to achieve your goal instead of using watchers.
methods: {
settings() {
// call this method from undo and redo methods if the conditions are met.
// move the watcher code here.
}
}
BTW,
If you don't use setter in computed properties, you don't need getters, so that is enough:
border_class() {
return this.settings.has_border ? ' rp-pwb' : ''
},
These watchers codes look belong to computed:
undoDisabled() {
return this.mutations.length;
},
redoDisabled() {
return this.mutations.length;
},
Related
I am trying to setup a button that changes a data value in Vue but also have it set using localStorage initally. This way I can have it keep the previous state it was in before a page refresh. Below is the code I'm using and I'm able to get it to work but know that it would be preferable to use the computed section but haven't been able to get that to work properly.
Would anyone know what is going wrong?
My button is triggered using the testing method and the variable in question is isGrid.
export default {
data() {
return {
option: 'default',
}
},
components: {
FileUploader,
},
mixins: [
visibilitiesMixin,
settingsMixin
],
props: {
vehicleId: {
type: Number,
required: true,
default: null,
}
},
computed: {
...mapState([
'isLoading',
'images',
'fallbackImageChecks',
'selectedImages'
]),
isGrid: {
get() {
return localStorage.getItem('isGrid');
},
},
imagesVModel: {
get() {
return this.images;
},
set(images) {
this.setImages(images);
}
},
selectedImagesVModel: {
get() {
return this.selectedImages;
},
set(images) {
this.setSelectedImages(images);
}
},
removeBgEnabled() {
return this.setting('nexus_integration_removebg_enabled') === 'enabled';
},
},
mounted() {
this.loadImages(this.vehicleId);
},
methods: {
testing() {
if (this.isGrid === 'false' || this.isGrid === false) {
localStorage.setItem('isGrid', true);
this.isGrid = true;
console.log(this.isGrid);
console.log(localStorage.getItem('isGrid'));
} else {
localStorage.setItem('isGrid', false);
this.isGrid = false;
console.log('b');
console.log(this.isGrid);
console.log(localStorage.getItem('isGrid'));
}
},
}
I suggest you use vuex with vuex-persistedstate.
https://www.npmjs.com/package/vuex-persistedstate
i'm working on small project using Vue.js i have created a pagination system to display my database users in a table, i have a small issue, i would like to know how can i stop the setinterval if my getResult function page variable is bigger than 1.
this is my code :
data(){
return {
editMode : true,
customer_id : null,
laravelData : {},
formFields : {}
}
},
methods:{
getResults(page = 1){
axios.get('Thirdparty/loadCustomers/' + page).then(response => {
this.laravelData = response.data;
});
}
},
created(){
self = this;
setInterval(function(){
self.getResults();
}, 5000);
}
First and foremost, always capture identifiers from setInterval and setTimeout.
By capturing your interval ID you can later remove it from within your callback when the page value is larger than its default (1).
EDIT: The OP would like to be able to reset the interval when page resets.
created() {
this.resetInterval();
},
methods: {
resetInterval() {
this.currentInterval && clearInterval(this.currentInterval);
this.currentInterval = setInterval(() => this.getResults(), 5000);
},
getResults(page = 1) {
if (page == 1 && !this.currentInterval) {
this.resetInterval();
} else {
clearInterval(this.currentInterval);
}
axios.get('Thirdparty/loadCustomers/' + page).then(response => {
this.laravelData = response.data;
});
}
}
data(){
return {
editMode : true,
customer_id : null,
laravelData : {},
formFields : {},
currentInterval : null
}
},
methods:{
getResults(page = 1){
clearInterval(this.currentInterval);
axios.get('Thirdparty/loadCustomers/' + page).then(response => {
this.laravelData = response.data;
});
},
created(){
self = this;
self.currentInterval = setInterval(function(){
self.getResults();
}, 5000);
}
I'm new to VueJs and currently trying to load some data only once and make it globally available to all vue components. What would be the best way to achieve this?
I'm a little bit stuck because the global variables occasionally seem to become null and I can't figure out why.
In my main.js I make three global Vue instance variables:
let globalData = new Vue({
data: {
$serviceDiscoveryUrl: 'http://localhost:40000/api/v1',
$serviceCollection: null,
$clientConfiguration: null
}
});
Vue.mixin({
computed: {
$serviceDiscoveryUrl: {
get: function () { return globalData.$data.$serviceDiscoveryUrl },
set: function (newUrl) { globalData.$data.$serviceDiscoveryUrl = newUrl; }
},
$serviceCollection: {
get: function () { return globalData.$data.$serviceCollection },
set: function (newCollection) { globalData.$data.$serviceCollection = newCollection; }
},
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) { globalData.$data.$clientConfiguration = newConfiguration; }
}
}
})
and in my App.vue component I load all the data:
<script>
export default {
name: 'app',
data: function () {
return {
isLoading: true,
isError: false
};
},
methods: {
loadAllData: function () {
this.$axios.get(this.$serviceDiscoveryUrl)
.then(
response => {
this.$serviceCollection = response.data;
let configurationService = this.$serviceCollection.services.find(obj => obj.key == "ProcessConfigurationService");
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
})
}
},
created: function m() {
this.loadAllData();
}
}
</script>
But when I try to access the $clientConfiguration it seems to be null from time to time and I can't figure out why. For example when I try to build the navigation sidebar:
beforeMount: function () {
let $ = JQuery;
let clients = [];
if (this.$clientConfiguration === null)
console.error("client config is <null>");
$.each(this.$clientConfiguration, function (key, clientValue) {
let processes = [];
$.each(clientValue.processConfigurations, function (k, processValue) {
processes.push(
{
name: processValue.name,
url: '/process/' + processValue.id,
icon: 'fal fa-project-diagram'
});
});
clients.push(
{
name: clientValue.name,
url: '/client/' + clientValue.id,
icon: 'fal fa-building',
children: processes
});
});
this.nav.find(obj => obj.name == 'Processes').children = clients;
The most likely cause is that the null is just the initial value. Loading the data is asynchronous so you'll need to wait for loading to finish before trying to create any components that rely on that data.
You have an isLoading flag, which I would guess is your attempt to wait for loading to complete before showing any components (maybe via a suitable v-if). However, it currently only waits for the first request and not the second. So this:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
would need to be:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
this.isLoading = false;
}
);
If it isn't that initial value that's the problem then you need to figure out what is setting it to null. That should be prety easy, just put a debugger statement in your setter:
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) {
if (!newConfiguration) {
debugger;
}
globalData.$data.$clientConfiguration = newConfiguration;
}
}
Beyond the problem with the null, if you're using Vue 2.6+ I would suggest taking a look at Vue.observable, which is a simpler way of creating a reactive object than creating a new Vue instance.
Personally I would probably implement all of this by putting a reactive object on Vue.prototype rather than using a global mixin. That assumes that you even need the object to be reactive, if you don't then this is all somewhat more complicated than it needs to be.
I am working on a small scale app that displays videos in multiple ways using a video-player component.
Currently I am implementing a stack-list, which is a container that holds video-stack components, and each stack contains one or more video-player components.
While the correct videos are loaded from the DOM, there is a noticeable multi-second lag (in terms of keyboard response) which seems to be related to the ending of the currently played video and the fetching of the next video in the stack.
How can I get rid of this lag? Videos are able to be toggled/selected via mouse hovers or WASD keyboard commands (A: previous, D: next), and the lag can cause a delay in keyboard inputs being registered.
video-stack.hbs
{{video-player highlightedStyle=(string-append stackStyle borderStyle)
looping=(is-single-video videos) videoPos=selectedVidPos
isMuted=(if (video-selected key selectedStackIndex) isMuted true)
url=(if curVideo.teaser.isUrl curVideo.teaser.fileIdentifier
(make-local-url modelIdentifier curVideo.teaser.fileIdentifier))
onClickCallback=(action 'stackClicked')
onHoverCallback=(action 'stackHovered')
onEndedCallback=(action 'getNextVid')}}
video-stack.js
import Ember from 'ember';
export default Ember.Component.extend({
selectedVidPos: 0,
selectedStackIndex: 0,
stackStyle: '',
playerSize: '',
isMuted: true,
init() {
this._super(...arguments);
switch(this.get('videos').length){
case 1:
break;
case 2:
this.set('stackStyle', 'vid-shadows--2');
break;
case 3:
this.set('stackStyle', 'vid-shadows--3');
break;
case 4:
this.set('stackStyle', 'vid-shadows--4');
break;
default:
this.set('stackStyle', 'vid-shadows--4');
break;
}
},
curVideo: Ember.computed('videos', 'selectedVidPos', function () {
return this.get('videos')[this.get('selectedVidPos')];
}),
actions: {
stackClicked() {
this.get('onClickCallback') (this.get('videos'), this.get('selectedVidPos'));
},
getNextVid() {
let arrayLength = this.get('videos').length;
//check if there is only 1 video in the stack
if (arrayLength === 1) {
return;
}
let curArrayPos = parseInt(this.get('selectedVidPos'));
this.set('selectedVidPos', (curArrayPos + 1) % arrayLength);
},
stackHovered() {
this.get('onHoverCallback') (this.get('videos'), this.get('selectedStackIndex'));
}
}
});
video-player.hbs
<video oncanplay={{action 'play'}} looping=true
onended={{action 'ended'}} src={{url}}
class="video-player__video {{highlightedStyle}} {{if playing '' 'video-
player__darken'}}" muted={{muted}} />
video-player.js
import Ember from 'ember';
export default Ember.Component.extend({
url: null,
looping: false,
playing: true,
muted: true,
highlightedStyle: '',
click(event) {
this.get('onClickCallback') (this.get('videoPos'));
event.stopPropagation();
},
mouseEnter() {
this.get('onHoverCallback') (this.get('videoPos'));
},
willClearRender() {
this.set('playingObserver', null);
this.set('urlObserver', null);
},
playingObserver: Ember.observer('playing', function() {
if (this) {
var p = this.get("playing");
var videoElement = this.$().find("video").get(0);
if (videoElement) {
if (p) {
videoElement.play();
}
else {
videoElement.pause();
}
}
else {
console.log("No video element found!");
}
}
}),
urlObserver: Ember.observer('url', function() {
if (this) {
var videoElement = this.$().find("video").get(0);
if (videoElement) {
videoElement.load();
}
else {
console.log("No video element found");
}
}
}),
actions: {
ended() {
if (this.get('looping')) {
this.$().find("video").get(0).play();
console.log("video-player ended");
}
else {
console.log(this.get('videoPos'));
this.get('onEndedCallback') (this.get('videoPos'));
}
},
play() {
if (this.get('playing')) {
this.$().find("video").get(0).play();
}
}
}
});
I can post more code if it would help shed light on the culprit, thanks!
I found the culprit of the lag. The issue was in the parent container, content-area.js, which had a resetTimeout action that was being called incorrectly, which caused the focus to cycle needlessly, resulting in the lag.
Also implemented a switch off in terms of rendering videos to ensure smooth loading from one video to the next in video-stack.js, there are now 2 video objects, A & B, which are fetched and preloaded from the blob object, showing one while the other is hidden. Once the displayed video ends, they swap out, and the next video in the stack is loaded.
video-stack.js
export default Ember.Component.extend({
selectedVidAPos: 0,
selectedVidBPos: 0,
selectedStackIndex: 0,
stackStyle: '',
playerSize: '',
isMuted: true,
showVidA: true,
init() {
...
}
},
videoA: Ember.computed('videos', 'selectedVidAPos', function () {
return this.get('videos')[this.get('selectedVidAPos')];
}),
videoB: Ember.computed('videos', 'selectedVidBPos', function () {
return this.get('videos')[this.get('selectedVidBPos')];
}),
actions: {
stackClicked() {
this.get('onClickCallback') (this.get('videos'), (this.get('showVidA') ? this.get('selectedVidAPos') : this.get('selectedVidBPos')));
},
getNextVideoA() {
let arrayLength = this.get('videos').length;
if (arrayLength === 1) {
return;
}
let curArrayPos = parseInt(this.get('selectedVidAPos'));
this.set('selectedVidAPos', (curArrayPos + 2) % arrayLength);
this.set('showVidA', false);
},
getNextVideoB(){
let arrayLength = this.get('videos').length;
if (arrayLength === 1) {
return;
}
let curArrayPos = parseInt(this.get('selectedVidBPos'));
this.set('selectedVidBPos', (curArrayPos + 2) % arrayLength);
this.set('showVidA', true);
},
stackHovered() {
this.get('onHoverCallback') (this.get('videos'), this.get('selectedStackIndex'));
}
}
});
content-area.js
import Ember from 'ember';
import KeyboardControls from '../mixins/keyboard-controls';
export default Ember.Component.extend(KeyboardControls, {
displayVideoSelect: false,
displayVideoSelectTimeout: null,
displayVideo: false,
video: null,
videoPlaying: false,
keyboard: null,
backgroundVideoPos: 0,
backgroundVideoUrl: null,
backgroundVideoKeys: null,
selectionVideos: [],
stackListData: null,
showVideoSelect: function() {
this.set('displayVideoSelect', true);
this.send('resetTimeout');
},
hideVideoSelect: function() {
this.set('displayVideoSelect', false);
clearTimeout(this.get('displayVideoSelectTimeout'));
},
pauseVideo: function() {
this.set('videoPlaying', !this.get('videoPlaying'));
this.set('displayVideoSelect', !this.get('videoPlaying'));
this.set('focus', this.get('videoPlaying'));
},
select: function() {
this.set('videoPlaying', false);
this.set('focus', false);
this.showVideoSelect();
this.send('resetTimeout');
},
cancel: function() {
this.pauseVideo();
this.send('resetTimeout');
},
goNext: function() {
this.pauseVideo();
this.send('resetTimeout');
},
goPrevious: function() {
this.pauseVideo();
this.send('resetTimeout');
},
updateFocus: function(param) {
if (param) {
this.$().attr('tabindex', 2);
this.$().focus();
}//if
else {
this.$().attr('tabindex', -2);
this.$().blur();
}//else
},
init() {
...
},
click() {
this.set('focus', false);
this.showVideoSelect();
},
actions: {
videoSelected(sender, videoData) {
...
},
videoEnded() {
this.set('focus', false);
this.showVideoSelect();
this.set('displayVideo', false);
},
cycleBackground() {
...
},
cancelPressed() {
this.cancel();
},
resetTimeout() {
let component = this;
clearTimeout(this.get('displayVideoSelectTimeout'));
let timeout = setTimeout(() => {
component.hideVideoSelect();
//This set command was responsible for the lag
component.set('focus', true);
}, this.get('data.config.ui.idle') * 1000);
this.set('displayVideoSelectTimeout', timeout);
}
}
});
I've got a parent component that has 2 child components;
UPDATE
I've rewritten some statements and code to make it more understandable.
Parent: ReservationFormComponent
Children: ReservationTypePanel & ReservationCalendarPanel
The parent component ReservationFormComponent initially displays the ReservationTypePanel only. The other sibling ReservationCalendarPanel is hidden until an item is selected on ReservationTypePanel.
So the problem is when an item is selected in ReservationTypePanel the ReservationCalendarPanel is rendered with initial values set in the ReservationFormStore store. Particularly
initialize: function(){
this.reservationType = void 8;
this.pickupTime = moment().add('minutes',30);
}
So when the ReservationCalendarPanel is rendered, its child Component DateTimeField which accepts the state pickupTime get re-rendered and fires up the onChange event which calls for another action
return DateTimeField({
pickupTime: pickupTime,
onChange: function(time){
// Here is where the action gets called again
this$.getFlux().actions.setReservationPickupTime(time);
}
});
And greets me with this error Uncaught Error: Cannot dispatch an action while another action is being dispatched
I've tried my best to trim down the codes below. I wasn't using JSX because the original code was in LiveScript so I just took the compiled code to display here instead.
This is the parent component ReservationFormComponent
ReservationFormComponent = React.createClass({
get flux(){ // Instantiating Fluxxor
return new Fluxxor.Flux({ // These are the stores
'reservation-form': new ReservationFormStore,
'reservation-types': new ReservationTypeStore
}, { // These are the actions
setReservationType: function(value){
return this.dispatch('SET_RESERVATION_TYPE', value);
},
setReservationPickupTime: function(value){
return this.dispatch('SET_RESERVATION_PICKUP_TIME', value);
}
});
},
componentWillMount: function(){
this.flux.store('reservation-form').addListener('change', this.onChange);
},
onChange: function(){ // This triggers the re-render to display the ReservationCalendarPanel
this.setState({
pickupTime: this.flux.store('reservation-form').pickupTime
});
},
render: function() {
reservationType = this.state.reservationType;
return form({
className: 'container'
}, ReservationTypePanel({
flux: this.flux
}), reservationType ? ReservationCalendarPanel({
flux: this.flux
}) : null // This conditional to mount or not mount the component
);
}
});
The ReservationTypePanel Component. Here, the rendered component listens to onClick event and dispatches setReservationType action.
ReservationTypePanel = React.createClass({
mixins: [fluxxor.FluxMixin(react)],
onSelectReservationType: function(reservationType){
var this$ = this;
return function(event){
this$.getFlux().actions.setReservationType(reservationType);
};
},
render: function() {
var this$ = this;
return ReservationTypeItem({
onClick: this$.onSelectReservationType(type);
})
}
});
The ReservationCalendarPanel Component. Here is where the DateTimeField is rendered and receives the state from the ReservationFormStore and sets the value which causes another dispatch. This is where the error comes.
ReservationCalendarPanel = React.createClass({
mixins: [fluxxor.FluxMixin(react)],
getInitialState: function() {
return {pickupTime: moment()} // sets the current time
},
componentWillMount: function(){
this.getFlux().store('reservation-form').addListener('change-pickup-time', this.onFlux);
},
componentWillUnmount: function(){
this.getFlux().store('reservation-form').removeListener('change-pickup-time', this.onFlux);
},
render: function() {
var this$ = this;
if (this.state.pickupTime) {
pickupTime = moment(this.state.pickupTime);
}
return DateTimeField({
date: pickupTime,
onChange: function(time){
// Here is where the action gets called again
this$.getFlux().actions.setReservationPickupTime(time);
}
});
});
This is the DateTimeField this is where the
DateTimeField = React.createClass({
getInitialState: function(){
return {
text: ''
};
},
componentWillReceiveProps: function(nextProps){
this.setDate(nextProps.date);
},
componentDidMount: function(){
$(this.getDOMNode()).datepicker()
.on('changeDate', this.onChangeDate)
.on('clearDate', this.onChangeDate);
this.setDate(this.props.date);
},
componentWillUnmount: function(){
return $(this.getDOMNode()).datepicker('remove');
},
getDatepickerDate: function(){
return $(this.getDOMNode()).datepicker('getDate');
},
setDate: function(date){
if (!this.isMounted()) {
return;
}
if (moment(date).isSame(this.getDatepickerDate, 'day')) {
// If there is no change between the date that
// is about to be set then just ignore and
// keep the old one.
return;
}
date = date ? moment(date).toDate() : void 8;
$(this.getDOMNode()).datepicker('setDate', date);
},
onChangeDate: function(event){
if (this.props.onChange) {
this.props.onChange(event.date);
}
},
render: function(){
return this.transferPropsTo(input({
type: 'text',
className: 'form-control'
}));
}
});
If in case here is the store:
ReservationFormStore = fluxxor.createStore({
actions: {
SET_RESERVATION_TYPE: 'setReservationType',
SET_RESERVATION_PICKUP_TIME: 'setPickupTime'
},
initialize: function(){
this.reservationType = void 8;
this.pickupTime = moment().add('minutes',30);
},
setReservationType: function(reservationType){
this.reservationType = reservationType;
this.reservationTypeValidate = true;
this.emit('change-reservation-type', this.reservationType);
this.emit('change');
}
setPickupTime: function(pickupTime){
this.pickupTime = pickupTime;
this.pickupTimeValidate = true;
this.emit('change-pickup-time', this.pickupTime);
this.emit('change');
}
});